[
  {
    "path": ".codebeatsettings",
    "content": "{\n  \"GOLANG\": {\n    \"TOO_MANY_IVARS\": [\n      15,\n      20,\n      22,\n      25,\n    ],\n    \"LOC\": [\n      50,\n      60,\n      70,\n      80,\n    ],\n    \"TOTAL_LOC\": [\n      350,\n      400,\n      500,\n      600\n    ],\n    \"TOO_MANY_FUNCTIONS\": [\n      40,\n      50,\n      55,\n      60,\n    ],\n  }\n}"
  },
  {
    "path": ".dockerignore",
    "content": "/k8s\n/change_logs\n.github\n.semaphore\n.vscode\nassets\n/dist\n/execs\n/notes\n/skins\nREADME.md\nLICENSE\ncov.out\n/k9s\n.travis.yml"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\ngithub: [derailed]\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_err.png\" align=\"right\" width=\"100\" height=\"auto\"/>\n\n<br/>\n<br/>\n<br/>\n\n**Describe the bug**\nA clear and concise description of what the bug is.\n\n**To Reproduce**\nSteps to reproduce the behavior:\n\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Historical Documents**\nWhen applicable please include any supporting artifacts: k9s debug logs, configurations, resource manifests, ...\n\n**Expected behavior**\nA clear and concise description of what you expected to happen.\n\n**Screenshots**\nIf applicable, add screenshots to help explain your problem.\n\n**Versions (please complete the following information):**\n\n- OS: [e.g. OSX]\n- K9s: [e.g. 0.1.0]\n- K8s: [e.g. 1.11.0]\n\n**Additional context**\nAdd any other context about the problem here.\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"100\" height=\"auto\"/>\n\n<br/>\n<br/>\n<br/>\n\n**Is your feature request related to a problem? Please describe.**\nA clear and concise description of what the problem is.\n\n**Describe the solution you'd like**\nA clear and concise description of what you want to happen.\n\n**Describe alternatives you've considered**\nA clear and concise description of any alternative solutions or features you've considered.\n\n**Additional context**\nAdd any other context or screenshots about the feature request here.\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "---\nversion: 2\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n\n  # Maintain dependencies for GitHub Actions\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: weekly\n\n  # Maintain dependencies for docker\n  - package-ecosystem: \"docker\"\n    directory: \"/\"\n    schedule:\n      interval: weekly\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: K9s Lint\n\non:\n  pull_request:\n    branches: [ master ]\n\njobs:\n  golangci:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout Code\n        uses: actions/checkout@v6.0.2\n\n      - name: Install Go\n        uses: actions/setup-go@v6.3.0\n        with:\n          go-version-file: go.mod\n          cache-dependency-path: go.sum\n\n      - name: Lint\n        uses: golangci/golangci-lint-action@v9.2.0\n        with:\n          github-token: ${{ secrets.GITHUB_TOKEN }}\n          version: v2.6"
  },
  {
    "path": ".github/workflows/stales-issues.yml",
    "content": "name: Closeout Stale Issues\non:\n  schedule:\n    - cron: \"0 2 * * *\"\n\njobs:\n  close-issues:\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n      pull-requests: write\n    steps:\n      - uses: actions/stale@v10\n        with:\n          days-before-issue-stale: 30\n          days-before-issue-close: 14\n          stale-issue-label: \"stale\"\n          stale-issue-message: \"This issue is stale because it has been open for 30 days with no activity.\"\n          close-issue-message: \"This issue was closed because it has been inactive for 14 days since being marked as stale.\"\n          repo-token: ${{ secrets.GITHUB_TOKEN }}"
  },
  {
    "path": ".github/workflows/stales-prs.yml",
    "content": "name: Closeout Stale PRs\non:\n  schedule:\n    - cron: \"0 2 * * *\"\n\njobs:\n  close-issues:\n    runs-on: ubuntu-latest\n    permissions:\n      issues: write\n      pull-requests: write\n    steps:\n      - uses: actions/stale@v10\n        with:\n          days-before-pr-stale: 30\n          days-before-pr-close: 14\n          stale-pr-label: \"stale\"\n          stale-pr-message: \"This PR is stale because it has been open for 30 days with no activity.\"\n          close-pr-message: \"This PR was closed because it has been inactive for 14 days since being marked as stale.\"\n          repo-token: ${{ secrets.GITHUB_TOKEN }}"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: K9s Test\n\non:\n  workflow_dispatch:\n    push:\n    branches:\n      - master\n    tags:\n      - rc*\n      - v*\n  pull_request:\n    branches:\n      - master\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout Code\n        uses: actions/checkout@v6.0.2\n\n      - name: Install Go\n        uses: actions/setup-go@v6.3.0\n        with:\n          go-version-file: go.mod\n          cache-dependency-path: go.sum\n\n      - name: Setup GO env\n        run: go env -w CGO_ENABLED=0\n\n      - name: Run Tests\n        run: make test\n        env:\n          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}\n"
  },
  {
    "path": ".gitignore",
    "content": ".vscode\n*.out\n.idea\n.envrc\ncov.out\nexecs\n/k9s\n/k8s\ndist\nnotes\nvendor\ngo.mod1\ngo.mod2\ngen.sh\n*.test\n*.log\n*~\npod1.go\n.project\nfaas\n.settings/*\ndemos\n/code\nkind\n*.snap\n/stresser\n__debug_bin*\nfg.yaml"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\n\nrun:\n  allow-parallel-runners: true\n\n  # timeout for analysis, e.g. 30s, 5m, default is 1m\n  timeout: 5m\n\n  # exit code when at least one issue was found, default is 1\n  issues-exit-code: 1\n  tests: true\n\nlinters:\n  disable:\n    - staticcheck\n  enable:\n    - sloglint\n    - bodyclose\n    - copyloopvar\n    - depguard\n    - errcheck\n    - errorlint\n    - gocheckcompilerdirectives\n    - gocritic\n    - godox\n    - goprintffuncname\n    - gosec\n    - govet\n    - intrange\n    - ineffassign\n    - misspell\n    - noctx\n    - nolintlint\n    - revive\n    - staticcheck\n    - testifylint\n    - unconvert\n    - unparam\n    - unused\n    - whitespace\n    - gocyclo\n    - funlen\n    - goconst\n    - dogsled\n    - lll\n\n  settings:\n    dogsled:\n      max-blank-identifiers: 3\n\n    gosec:\n      excludes:\n        - G109\n        - G115\n        - G204\n        - G303\n\n    sloglint:\n      no-mixed-args: true\n      kv-only: true\n      attr-only: false\n      no-global: \"\"\n      context: \"\"\n      static-msg: false\n      no-raw-keys: true\n      key-naming-case: camel\n      forbidden-keys:\n        - time\n        - level\n        - msg\n        - source\n      args-on-sep-lines: false\n\n    depguard:\n      rules:\n        logger:\n          deny:\n            # logging is allowed only by logutils.Log,\n            - pkg: \"github.com/sirupsen/logrus\"\n              desc: logging is allowed only by logutils.Log.\n            - pkg: \"github.com/pkg/errors\"\n              desc: Should be replaced by standard lib errors package.\n            - pkg: \"github.com/instana/testify\"\n              desc: It's a fork of github.com/stretchr/testify.\n          files:\n            - \"!**/pkg/logutils/**.go\"\n\n    dupl:\n      threshold: 100\n\n    funlen:\n      lines: -1\n      statements: 60\n\n    goconst:\n      min-len: 2\n      min-occurrences: 3\n      ignore-string-values:\n        - blee\n        - duh\n        - cl-1\n        - ct-1-1\n\n    gocritic:\n      enabled-tags:\n        - diagnostic\n        - experimental\n        - opinionated\n        - performance\n        - style\n      disabled-checks:\n        - dupImport # https://github.com/go-critic/go-critic/issues/845\n        - ifElseChain\n        - octalLiteral\n        - whyNoLint\n\n    gocyclo:\n      min-complexity: 35\n\n    godox:\n      keywords:\n        - FIXME\n\n    mnd:\n      checks:\n        - argument\n        - case\n        - condition\n        - return\n      ignored-numbers:\n        - '0'\n        - '1'\n        - '2'\n        - '3'\n      ignored-functions:\n        - strings.SplitN\n\n    govet:\n      settings:\n        printf:\n          funcs:\n            - (github.com/golangci/golangci-lint/v2/pkg/logutils.Log).Infof\n            - (github.com/golangci/golangci-lint/v2/pkg/logutils.Log).Warnf\n            - (github.com/golangci/golangci-lint/v2/pkg/logutils.Log).Errorf\n            - (github.com/golangci/golangci-lint/v2/pkg/logutils.Log).Fatalf\n      enable:\n        - nilness\n        - shadow\n\n    errorlint:\n      asserts: false\n\n    lll:\n      line-length: 170\n\n    misspell:\n      locale: US\n      ignore-rules:\n        - \"importas\"\n\n    nolintlint:\n      allow-unused: false\n      require-explanation: false\n      require-specific: true\n\n    revive:\n      rules:\n        - name: indent-error-flow\n        - name: unexported-return\n          disabled: true\n        - name: unused-parameter\n        - name: unused-receiver\n\n  exclusions:\n    presets:\n      - comments\n      - std-error-handling\n      - common-false-positives\n      - legacy\n    paths:\n      - test/testdata_etc # test files\n      - internal/go # extracted from Go code\n      - internal/x # extracted from x/tools code\n      - pkg/goformatters/gci/internal # extracted from gci code\n      - pkg/goanalysis/runner_checker.go # extracted from x/tools code\n    rules:\n      - path: (.+)_test\\.go\n        linters:\n          - dupl\n          - mnd\n          - lll\n\n      # Based on existing code, the modifications should be limited to make maintenance easier.\n      - path: pkg/golinters/unused/unused.go\n        linters: [gocritic]\n        text: \"rangeValCopy: each iteration copies 160 bytes \\\\(consider pointers or indexing\\\\)\"\n\n      # Related to the result of computation but divided multiple times by 1024.\n      - path: test/bench/bench_test.go\n        linters: [gosec]\n        text: \"G115: integer overflow conversion uint64 -> int\"\n\n      # The files created during the tests don't need to be secured.\n      - path: scripts/website/expand_templates/linters_test.go\n        linters: [gosec]\n        text: \"G306: Expect WriteFile permissions to be 0600 or less\"\n\n      # Related to migration command.\n      - path: pkg/commands/internal/migrate/two/\n        linters:\n          - lll\n\n      # Related to migration command.\n      - path: pkg/commands/internal/migrate/\n        linters:\n          - gocritic\n        text: \"hugeParam:\"\n\n      # The codes are close but this is not duplication.\n      - path: pkg/commands/(formatters|linters).go\n        linters:\n          - dupl\n\nformatters:\n  enable:\n    - gci\n    - gofmt\n    - goimports\n  settings:\n    gofmt:\n      rewrite-rules:\n        - pattern: 'interface{}'\n          replacement: 'any'\n    goimports:\n      local-prefixes:\n        - github.com/golangci/golangci-lint/v2\n  exclusions:\n    paths:\n      - test/testdata_etc # test files\n      - internal/go # extracted from Go code\n      - internal/x # extracted from x/tools code\n      - pkg/goformatters/gci/internal # extracted from gci code\n      - pkg/goanalysis/runner_checker.go # extracted from x/tools code\n"
  },
  {
    "path": ".goreleaser.yml",
    "content": "version: 2\n\nproject_name: k9s\n\nbefore:\n  hooks:\n    - go mod download\n    - go generate ./...\n\nrelease:\n  prerelease: false\n\nenv:\n  - CGO_ENABLED=0\n\nbuilds:\n  - id: linux\n    goos:\n      - linux\n    goarch:\n      - amd64\n      - arm64\n      - arm\n      - ppc64le\n      - s390x\n    goarm:\n      - 7\n    flags:\n      - -trimpath\n    ldflags:\n      - -s -w -X github.com/derailed/k9s/cmd.version=v{{.Version}}\n      - -s -w -X github.com/derailed/k9s/cmd.commit={{.Commit}}\n      - -s -w -X github.com/derailed/k9s/cmd.date={{.Date}}\n\n  - id: freebsd\n    goos:\n      - freebsd\n    goarch:\n      - amd64\n      - arm64\n    goarm:\n      - 7\n    flags:\n      - -trimpath\n    ldflags:\n      - -s -w -X github.com/derailed/k9s/cmd.version=v{{.Version}}\n      - -s -w -X github.com/derailed/k9s/cmd.commit={{.Commit}}\n      - -s -w -X github.com/derailed/k9s/cmd.date={{.Date}}\n\n  - id: osx\n    goos:\n      - darwin\n    goarch:\n      - amd64\n      - arm64\n    flags:\n      - -trimpath\n    ldflags:\n      - -s -w -X github.com/derailed/k9s/cmd.version=v{{.Version}}\n      - -s -w -X github.com/derailed/k9s/cmd.commit={{.Commit}}\n      - -s -w -X github.com/derailed/k9s/cmd.date={{.Date}}\n\n  - id: windows\n    goos:\n      - windows\n    goarch:\n      - amd64\n      - arm64\n    flags:\n      - -trimpath\n    ldflags:\n      - -s -w -X github.com/derailed/k9s/cmd.version=v{{.Version}}\n      - -s -w -X github.com/derailed/k9s/cmd.commit={{.Commit}}\n      - -s -w -X github.com/derailed/k9s/cmd.date={{.Date}}\n\narchives:\n  - name_template: >-\n      {{ .ProjectName }}_\n      {{- title .Os }}_\n      {{- if eq .Arch \"amd64\" }}amd64\n      {{- else if eq .Arch \"386\" }}i386\n      {{- else }}{{ .Arch }}{{ end }}\n      {{- if .Arm }}v{{ .Arm }}{{ end }}\n    format_overrides:\n      - goos: windows\n        formats: [\"zip\"]\n\nchecksum:\n  name_template: \"checksums.sha256\"\n\nsnapshot:\n  version_template: \"{{ .Tag }}-next\"\n\nchangelog:\n  sort: asc\n  filters:\n    exclude:\n      - \"^docs:\"\n      - \"^test:\"\n\nbrews:\n  - name: k9s\n    repository:\n      owner: derailed\n      name: homebrew-k9s\n    commit_author:\n      name: derailed\n      email: fernand@imhotep.io\n    directory: Formula\n    homepage: https://k9scli.io/\n    description: Kubernetes CLI To Manage Your Clusters In Style!\n    test: |\n      system \"k9s version\"\n\nnfpms:\n  - file_name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}'\n    maintainer: Fernand Galiana\n    homepage: https://k9scli.io\n    description: Kubernetes CLI To Manage Your Clusters In Style!\n    license: \"Apache-2.0\"\n    formats:\n      - deb\n      - rpm\n      - apk\n    bindir: /usr/bin\n    section: utils\n    contents:\n      - src: ./LICENSE\n        dst: /usr/share/doc/k9s/copyright\n        file_info:\n          mode: 0644\n\nsboms:\n  - artifacts: archive"
  },
  {
    "path": ".semaphore/semaphore.yml",
    "content": "version: v1.0\nname: First pipeline example\nagent:\n  machine:\n    type: e1-standard-2\n    os_image: ubuntu1804\n\nblocks:\n  - name: \"Build\"\n    task:\n      env_vars:\n        - name: APP_ENV\n          value: prod\n      jobs:\n      - name: Docker build\n        commands:\n          - checkout\n          - ls -1\n          - echo $APP_ENV\n          - echo \"Docker build...\"\n          - echo \"done\"\n\n  - name: \"Smoke tests\"\n    task:\n      jobs:\n      - name: Smoke\n        commands:\n          - checkout\n          - echo \"make smoke\"\n\n  - name: \"Unit tests\"\n    task:\n      jobs:\n      - name: RSpec\n        commands:\n          - checkout\n          - echo \"make rspec\"\n\n      - name: Lint code\n        commands:\n          - checkout\n          - echo \"make lint\"\n\n      - name: Check security\n        commands:\n          - checkout\n          - echo \"make security\"\n\n  - name: \"Integration tests\"\n    task:\n      jobs:\n      - name: Cucumber\n        commands:\n          - checkout\n          - echo \"make cucumber\"\n\n  - name: \"Push Image\"\n    task:\n      jobs:\n      - name: Push\n        commands:\n          - checkout\n          - echo \"make docker.push\"\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: go\ngo_import_path: github.com/derailed/k9s\ngo:\n  - \"1.23\"\n\njobs:\n  include:\n  - os: linux\n    arch: amd64\n  - os: linux\n    arch: ppc64le\n  - os: osx\n    arch: amd64\n\ndist: trusty\nsudo: false\n\ninstall: true\n\nscript:\n  - go build\n  - go test ./...\n"
  },
  {
    "path": "CNAME",
    "content": "k9scli.io"
  },
  {
    "path": "COPYING",
    "content": "   Copyright © 2019, Imhotep Software LLC <fernand@imhotep.io> and other contributors.\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "Dockerfile",
    "content": "# -----------------------------------------------------------------------------\n# The base image for building the k9s binary\nFROM --platform=$BUILDPLATFORM golang:1.25.5-alpine3.21 AS build\n\nARG TARGETOS\nARG TARGETARCH\nENV GOOS=$TARGETOS\nENV GOARCH=$TARGETARCH\n\nWORKDIR /k9s\nCOPY go.mod go.sum main.go Makefile ./\nCOPY internal internal\nCOPY cmd cmd\nRUN apk --no-cache add --update make libx11-dev git gcc libc-dev curl \\\n  && make build\n\n# -----------------------------------------------------------------------------\n# Build the final Docker image\nFROM --platform=$BUILDPLATFORM alpine:3.23.3\nARG KUBECTL_VERSION=\"v1.32.2\"\n\nCOPY --from=build /k9s/execs/k9s /bin/k9s\nRUN apk --no-cache add --update ca-certificates \\\n  && apk --no-cache add --update -t deps curl vim \\\n  && TARGET_ARCH=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \\\n  && curl -f -L https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/${TARGET_ARCH}/kubectl -o /usr/local/bin/kubectl \\\n  && chmod +x /usr/local/bin/kubectl \\\n  && apk del --purge deps\n\nENTRYPOINT [ \"/bin/k9s\" ]\n"
  },
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n"
  },
  {
    "path": "Makefile",
    "content": "NAME            := k9s\nVERSION         ?= v0.50.18\nPACKAGE         := github.com/derailed/$(NAME)\nOUTPUT_BIN      ?= execs/${NAME}\nGO_FLAGS        ?=\nGO_TAGS\t        ?= netgo\nCGO_ENABLED     ?=0\nGIT_REV         ?= $(shell git rev-parse --short HEAD)\n\nIMG_NAME        := derailed/k9s\nIMAGE           := ${IMG_NAME}:${VERSION}\nBUILD_PLATFORMS ?= linux/amd64,linux/arm64\n\nSOURCE_DATE_EPOCH ?= $(shell date +%s)\nifeq ($(shell uname), Darwin)\nDATE            ?= $(shell TZ=UTC /bin/date -j -f \"%s\" ${SOURCE_DATE_EPOCH} +\"%Y-%m-%dT%H:%M:%SZ\")\nelse\nDATE            ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +\"%Y-%m-%dT%H:%M:%SZ\")\nendif\n\ndefault: help\n\ntest:                    ## Run all tests\n\t@go clean --testcache && go test ./...\n\ncover:                   ## Run test coverage suite\n\t@go test ./... --coverprofile=cov.out\n\t@go tool cover --html=cov.out\n\nbuild:                   ## Builds the CLI\n\t@CGO_ENABLED=${CGO_ENABLED} go build ${GO_FLAGS} \\\n\t-ldflags \"-w -s -X ${PACKAGE}/cmd.version=${VERSION} -X ${PACKAGE}/cmd.commit=${GIT_REV} -X ${PACKAGE}/cmd.date=${DATE}\" \\\n\t-a -tags=${GO_TAGS} -o ${OUTPUT_BIN} main.go\n\nkubectl-stable-version:  ## Get kubectl latest stable version\n\t@curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt\n\nimgx:                    ## Build Docker Image\n\t@docker buildx build --platform ${BUILD_PLATFORMS} --rm -t ${IMAGE} --load .\n\npushx:                   ## Push Docker image to registry\n\t@docker buildx build --platform ${BUILD_PLATFORMS} --rm -t ${IMAGE} --push .\n\nhelp:\n\t@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = \":[^:]*?## \"}; {printf \"\\033[38;5;69m%-30s\\033[38;5;38m %s\\033[0m\\n\", $$1, $$2}'\n"
  },
  {
    "path": "README.md",
    "content": "<img src=\"assets/k9s.png\" alt=\"k9s\">\n\n## K9s - Kubernetes CLI To Manage Your Clusters In Style!\n\nK9s provides a terminal UI to interact with your Kubernetes clusters.\nThe aim of this project is to make it easier to navigate, observe and manage\nyour applications in the wild. K9s continually watches Kubernetes\nfor changes and offers subsequent commands to interact with your observed resources.\n\n---\n\n## Note...\n\nK9s is not pimped out by a big corporation with deep pockets.\nIt is a complex OSS project that demands a lot of my time to maintain and support.\nK9s will always remain OSS and therefore free! That said, if you feel k9s makes your day to day Kubernetes journey a tad brighter, saves you time and makes you more productive, please consider [sponsoring us!](https://github.com/sponsors/derailed)\nYour donations will go a long way in keeping our servers lights on and beers in our fridge!\n\n**Thank you!**\n\n---\n\n[![Go Report Card](https://goreportcard.com/badge/github.com/derailed/k9s?)](https://goreportcard.com/report/github.com/derailed/k9s)\n[![golangci badge](https://github.com/golangci/golangci-web/blob/master/src/assets/images/badge_a_plus_flat.svg)](https://golangci.com/r/github.com/derailed/k9s)\n[![Docker Pulls](https://img.shields.io/docker/pulls/derailed/k9s.svg?maxAge=604800)](https://hub.docker.com/r/derailed/k9s/)\n[![release](https://img.shields.io/github/release-pre/derailed/k9s.svg)](https://github.com/derailed/k9s/releases)\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/mum4k/termdash/blob/master/LICENSE)\n[![Releases](https://img.shields.io/github/downloads/derailed/k9s/total.svg)](https://github.com/derailed/k9s/releases)\n\n---\n\n## Screenshots\n\n1. Pods\n      <img src=\"assets/screen_po.png\"/>\n2. Logs\n      <img src=\"assets/screen_logs.png\"/>\n3. Deployments\n      <img src=\"assets/screen_dp.png\"/>\n\n---\n\n## Demo Videos/Recordings\n\n* [K9s v0.40.0 -Column Blow- Sneak peek](https://youtu.be/iy6RDozAM4A)\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n* [K9s v0.29.0](https://youtu.be/oiU3wmoAkBo)\n* [K9s v0.21.3](https://youtu.be/wG8KCwDAhnw)\n* [K9s v0.19.X](https://youtu.be/kj-WverKZ24)\n* [K9s v0.18.0](https://www.youtube.com/watch?v=zMnD5e53yRw)\n* [K9s v0.17.0](https://www.youtube.com/watch?v=7S33CNLAofk&feature=youtu.be)\n* [K9s Pulses](https://asciinema.org/a/UbXKPal6IWpTaVAjBBFmizcGN)\n* [K9s v0.15.1](https://youtu.be/7Fx4XQ2ftpM)\n* [K9s v0.13.0](https://www.youtube.com/watch?v=qaeR2iK7U0o&t=15s)\n* [K9s v0.9.0](https://www.youtube.com/watch?v=bxKfqumjW4I)\n* [K9s v0.7.0 Features](https://youtu.be/83jYehwlql8)\n* [K9s v0 Demo](https://youtu.be/k7zseUhaXeU)\n\n---\n\n## Documentation\n\nPlease refer to our [K9s documentation](https://k9scli.io) site for installation, usage, customization and tips.\n\n---\n\n## Slack Channel\n\nWanna discuss K9s features with your fellow `K9sers` or simply show your support for this tool?\n\n* Channel: [K9sersSlack](https://k9sers.slack.com/)\n* Invite: [K9slackers Invite](https://join.slack.com/t/k9sers/shared_invite/zt-3360a389v-ElLHrb0Dp1kAXqYUItSAFA)\n\n---\n\n## Installation\n\nK9s is available on Linux, macOS and Windows platforms.\nBinaries for Linux, Windows and Mac are available as tarballs in the [release page](https://github.com/derailed/k9s/releases).\n\n* Via [Homebrew](https://brew.sh/) for macOS or Linux\n\n   ```shell\n   brew install derailed/k9s/k9s\n   ```\n\n* Via [MacPorts](https://www.macports.org)\n\n   ```shell\n   sudo port install k9s\n   ```\n\n* Via [snap](https://snapcraft.io/k9s) for Linux\n\n  ```shell\n  snap install k9s --devmode\n  ```\n\n* On Arch Linux\n\n  ```shell\n  pacman -S k9s\n  ```\n\n* On OpenSUSE Linux distribution\n\n  ```shell\n  zypper install k9s\n  ```\n\n* On FreeBSD\n\n  ```shell\n  pkg install k9s\n  ```\n\n* On Ubuntu\n\n  ```shell\n  wget https://github.com/derailed/k9s/releases/latest/download/k9s_linux_amd64.deb && sudo apt install ./k9s_linux_amd64.deb && rm k9s_linux_amd64.deb\n  ```\n\n* On Fedora (42+)\n\n  ```shell\n  dnf install k9s\n  ```\n\n* Via [Winget](https://github.com/microsoft/winget-cli) for Windows\n\n  ```shell\n  winget install k9s\n  ```\n\n* Via [Scoop](https://scoop.sh) for Windows\n\n  ```shell\n  scoop install k9s\n  ```\n\n* Via [Chocolatey](https://chocolatey.org/packages/k9s) for Windows\n\n  ```shell\n  choco install k9s\n  ```\n\n* Via a GO install\n\n  ```shell\n  # NOTE: The dev version will be in effect!\n  go install github.com/derailed/k9s@latest\n  ```\n\n* Via [Webi](https://webinstall.dev) for Linux and macOS\n\n  ```shell\n  curl -sS https://webinstall.dev/k9s | bash\n  ```\n\n* Via [pkgx](https://pkgx.dev/pkgs/k9scli.io/) for Linux and macOS\n\n  ```shell\n  pkgx k9s\n  ```\n\n* Via [gah](https://github.com/marverix/gah) for Linux and macOS\n\n  ```shell\n  gah install k9s\n  ```\n\n* Via [Webi](https://webinstall.dev) for Windows\n\n  ```shell\n  curl.exe -A MS https://webinstall.dev/k9s | powershell\n  ```\n\n* As a [Docker Desktop Extension](https://docs.docker.com/desktop/extensions/) (for the Docker Desktop built in Kubernetes Server)\n\n  ```shell\n  docker extension install spurin/k9s-dd-extension:latest\n  ```\n\n---\n\n## Building From Source\n\n K9s is currently using GO v1.23.X or above.\n In order to build K9s from source you must:\n\n 1. Clone the repo\n 2. Build and run the executable\n\n      ```shell\n      make build && ./execs/k9s\n      ```\n\n---\n\n## Running with Docker\n\n### Running the official Docker image\n\n  You can run k9s as a Docker container by mounting your `KUBECONFIG`:\n\n  ```shell\n  docker run --rm -it -v $KUBECONFIG:/root/.kube/config derailed/k9s\n  ```\n\n  For default path it would be:\n\n  ```shell\n  docker run --rm -it -v ~/.kube/config:/root/.kube/config derailed/k9s\n  ```\n\n### Building your own Docker image\n\n  You can build your own Docker image of k9s from the [Dockerfile](Dockerfile) with the following:\n\n  ```shell\n  docker build -t k9s-docker:v0.0.1 .\n  ```\n\n  You can get the latest stable `kubectl` version and pass it to the `docker build` command with the `--build-arg` option.\n  You can use the `--build-arg` option to pass any valid `kubectl` version (like `v1.18.0` or `v1.19.1`).\n\n  ```shell\n  KUBECTL_VERSION=$(make kubectl-stable-version 2>/dev/null)\n  docker build --build-arg KUBECTL_VERSION=${KUBECTL_VERSION} -t k9s-docker:0.1 .\n  ```\n\n  Run your container:\n\n  ```shell\n  docker run --rm -it -v ~/.kube/config:/root/.kube/config k9s-docker:0.1\n  ```\n\n---\n\n## PreFlight Checks\n\n* K9s uses 256 colors terminal mode. On `Nix system make sure TERM is set accordingly.\n\n    ```shell\n    export TERM=xterm-256color\n    ```\n\n* In order to issue resource edit commands make sure your EDITOR and KUBE_EDITOR env vars are set.\n\n    ```shell\n    # Kubectl edit command will use this env var.\n    export KUBE_EDITOR=my_fav_editor\n    ```\n\n* K9s prefers recent kubernetes versions ie 1.28+\n\n---\n\n## K8S Compatibility Matrix\n\n|         k9s        | k8s client |\n| ------------------ | ---------- |\n|     >= v0.27.0     |   1.26.1   |\n| v0.26.7 - v0.26.6  |   1.25.3   |\n| v0.26.5 - v0.26.4  |   1.25.1   |\n| v0.26.3 - v0.26.1  |   1.24.3   |\n| v0.26.0 - v0.25.19 |   1.24.2   |\n| v0.25.18 - v0.25.3 |   1.22.3   |\n| v0.25.2 - v0.25.0  |   1.22.0   |\n|      <= v0.24      |   1.21.3   |\n\n---\n\n## The Command Line\n\n```shell\n# List current version\nk9s version\n\n# To get info about K9s runtime (logs, configs, etc..)\nk9s info\n\n# List all available CLI options\nk9s help\n\n# To run K9s in a given namespace\nk9s -n mycoolns\n\n# Start K9s in an existing KubeConfig context\nk9s --context coolCtx\n\n# Start K9s in readonly mode - with all cluster modification commands disabled\nk9s --readonly\n```\n\n## Logs And Debug Logs\n\nGiven the nature of the ui k9s does produce logs to a specific location.\nTo view the logs and turn on debug mode, use the following commands:\n\n```shell\n# Find out where the logs are stored\nk9s info\n```\n\n```text\n ____  __.________\n|    |/ _/   __   \\______\n|      < \\____    /  ___/\n|    |  \\   /    /\\___ \\\n|____|__ \\ /____//____  >\n        \\/            \\/\n\nVersion:           vX.Y.Z\nConfig:            /Users/fernand/.config/k9s/config.yaml\nLogs:              /Users/fernand/.local/state/k9s/k9s.log\nDumps dir:         /Users/fernand/.local/state/k9s/screen-dumps\nBenchmarks dir:    /Users/fernand/.local/state/k9s/benchmarks\nSkins dir:         /Users/fernand/.local/share/k9s/skins\nContexts dir:      /Users/fernand/.local/share/k9s/clusters\nCustom views file: /Users/fernand/.local/share/k9s/views.yaml\nPlugins file:      /Users/fernand/.local/share/k9s/plugins.yaml\nHotkeys file:      /Users/fernand/.local/share/k9s/hotkeys.yaml\nAlias file:        /Users/fernand/.local/share/k9s/aliases.yaml\n```\n\n### View K9s logs\n\n```shell\ntail -f /Users/fernand/.local/data/k9s/k9s.log\n```\n\n### Start K9s in debug mode\n\n```shell\nk9s -l debug\n```\n\n### Customize logs destination\n\nYou can override the default log file destination either with the `--logFile` argument:\n\n```shell\nk9s --logFile /tmp/k9s.log\nless /tmp/k9s.log\n```\n\nOr through the `K9S_LOGS_DIR` environment variable:\n\n```shell\nK9S_LOGS_DIR=/var/log k9s\nless /var/log/k9s.log\n```\n\n## Key Bindings\n\nK9s uses aliases to navigate most K8s resources.\n\n| Action                                                                          | Command                       | Comment                                                                |\n|---------------------------------------------------------------------------------|-------------------------------|------------------------------------------------------------------------|\n| Show active keyboard mnemonics and help                                         | `?`                           |                                                                        |\n| Show all available resource alias                                               | `ctrl-a`                      |                                                                        |\n| To bail out of K9s                                                              | `:quit`, `:q`, `ctrl-c`       |                                                                        |\n| To go up/back to the previous view                                              | `esc`                         | If you have crumbs on, this will go to the previous one                |\n| View a Kubernetes resource using singular/plural or short-name                  | `:`pod⏎                       | accepts singular, plural, short-name or alias ie pod or pods           |\n| View a Kubernetes resource in a given namespace                                 | `:`pod ns-x⏎                  |                                                                        |\n| View filtered pods (New v0.30.0!)                                               | `:`pod /fred⏎                 | View all pods filtered by fred                                         |\n| View labeled pods (New v0.30.0!)                                                | `:`pod app=fred,env=dev⏎      | View all pods with labels matching app=fred and env=dev                |\n| View pods in a given context (New v0.30.0!)                                     | `:`pod @ctx1⏎                 | View all pods in context ctx1. Switches out your current k9s context!  |\n| Filter out a resource view given a filter                                       | `/`filter⏎                    | Regex2 supported ie `fred|blee` to filter resources named fred or blee |\n| Inverse regex filter                                                            | `/`! filter⏎                  | Keep everything that *doesn't* match.                                  |\n| Filter resource view by labels                                                  | `/`-l label-selector⏎         |                                                                        |\n| Fuzzy find a resource given a filter                                            | `/`-f filter⏎                 |                                                                        |\n| Bails out of view/command/filter mode                                           | `<esc>`                       |                                                                        |\n| To view and switch to another Kubernetes context (Pod view)                     | `:`ctx⏎                       |                                                                        |\n| To view and switch directly to another Kubernetes context (Last used view)      | `:`ctx context-name⏎          |                                                                        |\n| To view and switch to another Kubernetes namespace                              | `:`ns⏎                        |                                                                        |\n| To switch back to the last active command (like how \"cd -\" works)               | `-`                           | Navigation that adds breadcrumbs to the bottom are not commands        |\n| To go back and forward through the command history                              | back: `[`, forward: `]`       | Same as above                                                          |\n| To view all saved resources                                                     | `:`screendump or sd⏎          |                                                                        |\n| To delete a resource (TAB and ENTER to confirm)                                 | `ctrl-d`                      |                                                                        |\n| To kill a resource (no confirmation dialog, equivalent to kubectl delete --now) | `ctrl-k`                      |                                                                        |\n| Launch pulses view                                                              | `:`pulses or pu⏎              |                                                                        |\n| Launch XRay view                                                                | `:`xray RESOURCE [NAMESPACE]⏎  | RESOURCE can be one of po, svc, dp, rs, sts, ds, NAMESPACE is optional |\n| Launch Popeye view                                                              | `:`popeye or pop⏎              | See [popeye](#popeye)                                                  |\n| Mark resource                                                                   | `space`                        |                                                                        |\n| Mark range of resources                                                         | `ctrl-space`                   |                                                                        |\n| Clear all marks                                                                 | `ctrl-\\`                       |                                                                        |\n| Save resources to file                                                          | `ctrl-s`                       |                                                                        |\n| Toggle faults/error display                                                     | `ctrl-z`                       |                                                                        |\n| Toggle wide columns                                                             | `ctrl-w`                       |                                                                        |\n| Toggle header                                                                   | `ctrl-e`                       |                                                                        |\n| Toggle breadcrumbs                                                              | `ctrl-g`                       |                                                                        |\n| Move selected column left                                                       | `shift-left arrow`             |                                                                        |\n| Move selected column right                                                      | `shift-right arrow`            |                                                                        |\n| Sort by selected column                                                         | `shift-o`                      |                                                                        |\n| Sort by Name                                                                    | `shift-n`                      |                                                                        |\n| Sort by Age                                                                     | `shift-a`                      |                                                                        |\n| Sort by Namespace                                                               | `shift-p`                      | Only when viewing all namespaces                                       |\n| Sort by Status                                                                  | `shift-s`                      |                                                                        |\n| Copy resource name                                                              | `c`                            |                                                                        |\n| Copy namespace                                                                  | `n`                            |                                                                        |\n| View YAML                                                                       | `y`                            |                                                                        |\n| View logs                                                                       | `l`                            | Resource specific                                                      |\n| View previous logs                                                              | `p`                            | Resource specific                                                      |\n| Shell into container                                                            | `s`                            | Pods only                                                              |\n| Attach to container                                                             | `a`                            | Pods only                                                              |\n| Describe resource                                                               | `d`                            |                                                                        |\n| Edit resource                                                                   | `e`                            | Not available in read-only mode                                        |\n| Show port-forwards                                                              | `f`                            | Pods/Services/Containers                                               |\n| Port forward                                                                    | `shift-f`                      | Pods/Services/Containers                                               |\n| Warp to namespace                                                               | `w`                            | When namespace column is available                                     |\n| Jump to owner                                                                   | `shift-j`                      | When resource has an owner                                             |\n| Use/switch namespace                                                            | `u`                            | Namespace view                                                         |\n| UsedBy (show resources using this)                                              | `u`                            | ServiceAccounts/PVCs/Secrets/ConfigMaps                                |\n| Benchmark (run/stop)                                                            | `b`                            | Services/Port-forwards                                                 |\n| Toggle text wrap                                                                | `w`                            | Log view                                                               |\n| Toggle timestamp                                                                | `t`                            | Log view                                                               |\n| Toggle fullscreen                                                               | `f`                            | Log/YAML/Details view                                                  |\n| Refresh/reload view                                                             | `ctrl-r`                       |                                                                        |\n| Trigger (CronJob)                                                               | `t`                            | CronJob view                                                           |\n| Cordon/Uncordon node                                                            | `u`                            | Node view                                                              |\n| Drain node                                                                      | `r`                            | Node view                                                              |\n| Restart resource                                                                | `r`                            | Deployments/DaemonSets/StatefulSets                                    |\n| Rollback resource                                                               | `ctrl-l`                       | ReplicaSets                                                            |\n| View ReplicaSets                                                                | `z`                            | Deployment view                                                        |\n\n---\n\n## K9s Configuration\n\n  K9s keeps its configurations as YAML files inside of a `k9s` directory and the location depends on your operating system. K9s leverages [XDG](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) to load its various configurations files. For information on the default locations for your OS please see [this link](https://github.com/adrg/xdg/blob/master/README.md). If you are still confused a quick `k9s info` will reveal where k9s is loading its configurations from. Alternatively, you can set `K9S_CONFIG_DIR` to tell K9s the directory location to pull its configurations from.\n\n  | Unix            | macOS                              | Windows               |\n  |-----------------|------------------------------------|-----------------------|\n  | `~/.config/k9s` | `~/Library/Application Support/k9s` | `%LOCALAPPDATA%\\k9s`  |\n\n  > NOTE: This is still in flux and will change while in pre-release stage!\n\nYou can now override the context portForward default address configuration by setting an env variable that can override all clusters portForward local address using `K9S_DEFAULT_PF_ADDRESS=a.b.c.d`\n\n  ```yaml\n  # $XDG_CONFIG_HOME/k9s/config.yaml\n  k9s:\n    # Enable periodic refresh of resource browser windows. Default false\n    liveViewAutoRefresh: false\n    # !!New!! v0.50.8...\n    # Extends the list of supported GPU vendors. The key is the vendor name, the value must correspond to k8s resource driver designation.\n    # Default known GPU vendors:\n    # nvidia: nvidia.com/gpu\n\t  # nvidia-shared: nvidia.com/gpu.shared\n\t  # amd: amd.com/gpu\n\t  # intel: gpu.intel.com/i915\n    gpuVendors:\n      bozo: bozo/gpu  # extends the gpu vendor and add \"bozo\"\n    # The path to screen dump. Default: '%temp_dir%/k9s-screens-%username%' (k9s info)\n    screenDumpDir: /tmp/dumps\n    # Represents ui poll intervals in seconds. Default 2.0 secs. Minimum value is 2.0 - values below will be capped to the minimum.\n    refreshRate: 2\n    # Overrides the default k8s api server requests timeout. Defaults 120s\n    apiServerTimeout: 15s\n    # Number of retries once the connection to the api-server is lost. Default 15.\n    maxConnRetry: 5\n    # Indicates whether modification commands like delete/kill/edit are disabled. Default is false\n    readOnly: false\n    # This setting allows users to specify the default view, but it is not set by default.\n    defaultView: \"\"\n    # Toggles whether k9s should exit when CTRL-C is pressed. When set to true, you will need to exit k9s via the :quit command. Default is false.\n    noExitOnCtrlC: false\n    #UI settings\n    ui:\n      # Enable mouse support. Default false\n      enableMouse: false\n      # Set to true to hide K9s header. Default false\n      headless: false\n      # Set to true to hide the K9S logo Default false\n      logoless: false\n      # Set to true to hide K9s crumbs. Default false\n      crumbsless: false\n      # Set to true to suppress the K9s splash screen on start. Default false. Note that for larger clusters or higher latency connections, there may be no resources visible initially until local caches have finished populating.\n      splashless: false\n      # Toggles icons display as not all terminal support these chars. Default: true\n      noIcons: false\n      # Toggles reactive UI. This option provide for watching on disk artifacts changes and update the UI live Defaults to false.\n      reactive: false\n      # By default all contexts will use the dracula skin unless explicitly overridden in the context config file.\n      skin: dracula # => assumes the file skins/dracula.yaml is present in the  $XDG_DATA_HOME/k9s/skins directory. Can be overriden with K9S_SKIN.\n      # Convert dark skins to light, or vice versa, preserving hue. Default: false\n      invert: false\n      # Allows to set certain views default fullscreen mode. (yaml, helm history, describe, value_extender, details, logs) Default false\n      defaultsToFullScreen: false\n      # Show full resource GVR (Group/Version/Resource) vs just R. Default: false.\n      useFullGVRTitle: false\n    # Toggles icons display as not all terminal support these chars.\n    noIcons: false\n    # Toggles whether k9s should check for the latest revision from the GitHub repository releases. Default is false.\n    skipLatestRevCheck: false\n    # When altering kubeconfig or using multiple kube configs, k9s will clean up clusters configurations that are no longer in use. Setting this flag to true will keep k9s from cleaning up inactive cluster configs. Defaults to false.\n    keepMissingClusters: false\n    # Logs configuration\n    logger:\n      # Defines the number of lines to return. Default 100\n      tail: 200\n      # Defines the total number of log lines to allow in the view. Default 1000\n      buffer: 500\n      # Represents how far to go back in the log timeline in seconds. Setting to -1 will tail logs. Default is -1.\n      sinceSeconds: 300 # => tail the last 5 mins.\n      # Toggles log line wrap. Default false\n      textWrap: false\n      # Autoscroll in logs will be disabled. Default is false.\n      disableAutoscroll: false\n      # Enable column locking when autoscroll is enabled. Default is false.\n      columnLock: false\n      # Toggles log line timestamp info. Default false\n      showTime: false\n    # Provide shell pod customization when nodeShell feature gate is enabled!\n    shellPod:\n      # The shell pod image to use.\n      image: killerAdmin\n      # The namespace to launch to shell pod into.\n      namespace: default\n      # The resource limit to set on the shell pod.\n      limits:\n        cpu: 100m\n        memory: 100Mi\n      # Enable TTY\n      tty: true\n      hostPathVolume:\n      - name: docker-socket\n        # Mount the Docker socket into the shell pod\n        mountPath: /var/run/docker.sock\n        # The path on the host to mount\n        hostPath: /var/run/docker.sock\n        readOnly: true\n  ```\n\n---\n\n## <a id=\"popeye\"></a>Popeye Configuration\n\nK9s has integration with [Popeye](https://popeyecli.io/), which is a Kubernetes cluster sanitizer.  Popeye itself uses a configuration called `spinach.yml`, but when integrating with K9s the cluster-specific file should be name `$XDG_CONFIG_HOME/share/k9s/clusters/clusterX/contextY/spinach.yml`.  This allows you to have a different spinach config per cluster.\n\n---\n\n## Node Shell\n\nBy enabling the nodeShell feature gate on a given cluster, K9s allows you to shell into your cluster nodes. Once enabled, you will have a new `s` for `shell` menu option while in node view. K9s will launch a pod on the selected node using a special k9s_shell pod. Furthermore, you can refine your shell pod by using a custom docker image preloaded with the shell tools you love. By default k9s uses a BusyBox image, but you can configure it as follows:\n\nAlternatively, you can now override the context configuration by setting an env variable that can override all clusters node shell gate using `K9S_FEATURE_GATE_NODE_SHELL=true|false`\n\n```yaml\n# $XDG_CONFIG_HOME/k9s/config.yaml\nk9s:\n  # You can also further tune the shell pod specification\n  shellPod:\n    image: cool_kid_admin:42\n    namespace: blee\n    limits:\n      cpu: 100m\n      memory: 100Mi\n```\n\nThen in your cluster configuration file...\n\n```yaml\n# $XDG_DATA_HOME/k9s/clusters/cluster-1/context-1\nk9s:\n  cluster: cluster-1\n  readOnly: false\n  namespace:\n    active: default\n    lockFavorites: false\n    favorites:\n    - kube-system\n    - default\n  view:\n    active: po\n  featureGates:\n    nodeShell: true # => Enable this feature gate to make nodeShell available on this cluster\n  portForwardAddress: localhost\n```\n\n### Customizing the Shell Pod\nYou can also customize the shell pod by adding a `hostPathVolume` to your shell pod. This allows you to mount a local directory or file into the shell pod. For example, if you want to mount the Docker socket into the shell pod, you can do so as follows:\n```yaml\nk9s:\n  shellPod:\n    hostPathVolume:\n    - name: docker-socket\n      # Mount the Docker socket into the shell pod\n      mountPath: /var/run/docker.sock\n      # The path on the host to mount\n      hostPath: /var/run/docker.sock\n      readOnly: true\n```\nThis will mount the Docker socket into the shell pod at `/var/run/docker.sock` and make it read-only. You can also mount any other directory or file in a similar way.\n\n---\n\n## Command Aliases\n\nIn K9s, you can define your very own command aliases (shortnames) to access your resources. In your `$HOME/.config/k9s` define a file called `aliases.yaml`.\nA K9s alias defines pairs of alias:gvr. A gvr (Group/Version/Resource) represents a fully qualified Kubernetes resource identifier. Here is an example of an alias file:\n\n```yaml\n#  $XDG_DATA_HOME/k9s/aliases.yaml\naliases:\n  pp: v1/pods\n  crb: rbac.authorization.k8s.io/v1/clusterrolebindings\n  # As of v0.30.0 you can also refer to another command alias...\n  fred: pod fred app=blee # => view pods in namespace fred with labels matching app=blee\n```\n\nUsing this aliases file, you can now type `:pp` or `:crb` or `:fred` to activate their respective commands.\n\n---\n\n## HotKey Support\n\nEntering the command mode and typing a resource name or alias, could be cumbersome for navigating thru often used resources.\nWe're introducing hotkeys that allow users to define their own key combination to activate their favorite resource views.\n\nAdditionally, you can define context specific hotkeys by add a context level configuration file in `$XDG_DATA_HOME/k9s/clusters/clusterX/contextY/hotkeys.yaml`\n\nIn order to surface hotkeys globally please follow these steps:\n\n1. Create a file named `$XDG_CONFIG_HOME/k9s/hotkeys.yaml`\n2. Add the following to your `hotkeys.yaml`. You can use resource name/short name to specify a command ie same as typing it while in command mode.\n\n      ```yaml\n      #  $XDG_CONFIG_HOME/k9s/hotkeys.yaml\n      hotKeys:\n        # Hitting Shift-0 navigates to your pod view\n        shift-0:\n          shortCut:    Shift-0\n          description: Viewing pods\n          command:     pods\n        # Hitting Shift-1 navigates to your deployments\n        shift-1:\n          shortCut:    Shift-1\n          description: View deployments\n          command:     dp\n        # Hitting Shift-2 navigates to your xray deployments\n        shift-2:\n          shortCut:    Shift-2\n          description: Xray Deployments\n          command:     xray deploy\n        # Hitting Shift-S view the resources in the namespace of your current selection\n        shift-s:\n          shortCut:    Shift-S\n          override:    true # => will override the default shortcut related action if set to true (default to false)\n          description: Namespaced resources\n          command:     \"$RESOURCE_NAME $NAMESPACE\"\n          keepHistory: true # whether you can return to the previous view\n      ```\n\n Not feeling so hot? Your custom hotkeys will be listed in the help view `?`.\n Also your hotkeys file will be automatically reloaded so you can readily use your hotkeys as you define them.\n\n You can choose any keyboard shortcuts that make sense to you, provided they are not part of the standard K9s shortcuts list.\n\n Similarly, referencing environment variables in hotkeys is also supported. The available environment variables can refer to the description in the [Plugins](#plugins) section.\n\n> NOTE: This feature/configuration might change in future releases!\n\n---\n\n## Port Forwarding over websockets\n\nK9s follows `kubectl` feature flag environment variables to enable/disable port-forwarding over websockets. (default enabled in >1.30)\nTo disable Websocket support, set `KUBECTL_PORT_FORWARD_WEBSOCKETS=false`\n\n---\n\n## FastForwards\n\nAs of v0.25.0, you can leverage the `FastForwards` feature to tell K9s how to default port-forwards. In situations where you are dealing with multiple containers or containers exposing multiple ports, it can be cumbersome to specify the desired port-forward from the dialog as in most cases, you already know which container/port tuple you desire. For these use cases, you can now annotate your manifests with the following annotations:\n\n@ `k9scli.io/auto-port-forwards`\n  activates one or more port-forwards directly bypassing the port-forward dialog all together.\n@ `k9scli.io/port-forwards`\n  pre-selects one or more port-forwards when launching the port-forward dialog.\n\nThe annotation value takes on the shape `container-name::[local-port:]container-port`\n\n> NOTE: for either cases above you can specify the container port by name or number in your annotation!\n\n### Example\n\n```yaml\n# Pod fred\napiVersion: v1\nkind: Pod\nmetadata:\n  name: fred\n  annotations:\n    k9scli.io/auto-port-forwards: zorg::5556        # => will default to container zorg port 5556 and local port 5566. No port-forward dialog will be shown.\n    # Or...\n    k9scli.io/port-forwards: bozo::9090:p1          # => launches the port-forward dialog selecting default port-forward on container bozo port named p1(8081)\n                                                    # mapping to local port 9090.\n    ...\nspec:\n  containers:\n  - name: zorg\n    ports:\n    - name: p1\n      containerPort: 5556\n    ...\n  - name: bozo\n    ports:\n    - name: p1\n      containerPort: 8081\n    - name: p2\n      containerPort: 5555\n    ...\n```\n\nThe annotation value must specify a container to forward to as well as a local port and container port. The container port may be specified as either a port number or port name. If the local port is omitted then the local port will default to the container port number. Here are a few examples:\n\n1. bozo::http      - creates a pf on container `bozo` with port name http. If http specifies port number 8080 then the local port will be 8080 as well.\n2. bozo::9090:http - creates a pf on container `bozo` mapping local port 9090->http(8080)\n3. bozo::9090:8080 - creates a pf on container `bozo` mapping local port 9090->8080\n\n---\n\n## Custom Views\n\n[SneakCast v0.17.0 on The Beach! - Yup! sound is sucking but what a setting!](https://youtu.be/7S33CNLAofk)\n\nYou can change which columns shows up for a given resource via custom views. To surface this feature, you will need to create a new configuration file, namely `$XDG_CONFIG_HOME/k9s/views.yaml`. This file leverages GVR (Group/Version/Resource) to configure the associated table view columns. If no GVR is found for a view the default rendering will take over (ie what we have now). Going wide will add all the remaining columns that are available on the given resource after your custom columns. To boot, you can edit your views config file and tune your resources views live!\n\n📢 🎉 As of `release v0.40.0` you can specify json parse expressions to further customize your resources rendering.\n\nThe new column syntax is as follows:\n\n> COLUMN_NAME<:json_parse_expression><|column_attributes>\n\nWhere `:json_parse_expression` represents an expression to pull a specific snippet out of the resource manifest.\nSimilar to `kubectl -o custom-columns` command. This expression is optional.\n\n> IMPORTANT! Columns must be valid YAML strings. Thus if your column definition contains non-alpha chars\n> they must figure with either single/double quotes or escaped via `\\`\n\n> NOTE! Be sure to watch k9s logs as any issues with the custom views specification are only surfaced in the logs.\n\nAdditionally, you can specify column attributes to further tailor the column rendering.\nTo use this you will need to add a `|` indicator followed by your rendering bits.\nYou can have one or more of the following attributes:\n\n* `T` -> time column indicator\n* `N` -> number column indicator\n* `W` -> turns on wide column aka only shows while in wide mode. Defaults to the standard resource definition when present.\n* `S` -> Ensures a column is visible and not wide. Overrides `wide` std resource definition if present.\n* `H` -> Hides the column\n* `L` -> Left align (default)\n* `R` -> Right align\n\nHere is a sample views configuration that customize a pods and services views.\n\n```yaml\n# $XDG_CONFIG_HOME/k9s/views.yaml\nviews:\n  v1/pods:\n    columns:\n      - AGE\n      - NAMESPACE|WR                                     # => 🌚 Specifies the NAMESPACE column to be right aligned and only visible while in wide mode\n      - ZORG:.metadata.labels.fred\\.io\\.kubernetes\\.blee # => 🌚 extract fred.io.kubernetes.blee label into it's own column\n      - BLEE:.metadata.annotations.blee|R                # => 🌚 extract annotation blee into it's own column and right align it\n      - NAME\n      - IP\n      - NODE\n      - STATUS\n      - READY\n      - MEM/RL|S                                         # => 🌚 Overrides std resource default wide attribute via `S` for `Show`\n      - '%MEM/R|'                                        # => NOTE! column names with non alpha names need to be quoted as columns must be strings!\n\n  v1/pods@fred:                                          # => 🌚 New v0.40.6! Customize columns for a given resource and namespace!\n    columns:\n      - AGE\n      - NAME|WR\n\n  v1/pods@kube*:                                         # => 🌚 New v0.40.6! You can also specify a namespace using a regular expression.\n    columns:\n      - NAME\n      - AGE\n      - LABELS\n\n  cool-kid:                                              # => 🌚 New v0.40.8! You can also reference a specific alias and display a custom view for it\n    columns:\n      - AGE\n      - NAMESPACE|WR\n\n  v1/services:\n    columns:\n      - AGE\n      - NAMESPACE\n      - NAME\n      - TYPE\n      - CLUSTER-IP\n```\n\n> 🩻 NOTE: This is experimental and will most likely change as we iron this out!\n\n---\n\n## Plugins\n\nK9s allows you to extend your command line and tooling by defining your very own cluster commands via plugins.\nMinimally we look at `$XDG_CONFIG_HOME/k9s/plugins.yaml` to locate all available plugins.\nAdditionally, K9s will scan the following directories for additional plugins:\n\n* `$XDG_CONFIG_HOME/k9s/plugins`\n* `$XDG_DATA_HOME/k9s/plugins`\n* `$XDG_DATA_DIRS/k9s/plugins`\n\nThe plugin file content can be either a single plugin snippet, a collections of snippets or a complete plugins definition (see examples below...).\n\nA plugin is defined as follows:\n\n* Shortcut option represents the key combination a user would type to activate the plugin. Valid values are [a-z], Shift-[A-Z], Ctrl-[A-Z].\n* Override option make that the default action related to the shortcut will be overridden by the plugin\n* Confirm option (when enabled) lets you see the command that is going to be executed and gives you an option to confirm or prevent execution\n* Description will be printed next to the shortcut in the k9s menu\n* Scopes defines a collection of resources names/short-names for the views associated with the plugin. You can specify `all` to provide this shortcut for all views.\n* Command represents ad-hoc commands the plugin runs upon activation\n* Background specifies whether or not the command runs in the background\n* Args specifies the various arguments that should apply to the command above\n* OverwriteOutput boolean option allows plugin developers to provide custom messages on plugin stdout execution. See example in [#2644](https://github.com/derailed/k9s/pull/2644)\n* Dangerous boolean option enables disabling the plugin when read-only mode is set. See [#2604](https://github.com/derailed/k9s/issues/2604)\n* Inputs defines a list of input fields to prompt the user for before executing the plugin (see below)\n\n#### Plugin Inputs\n\nPlugins can define input fields that prompt users for values before execution. This is useful when you need dynamic values like replica counts, environment variables, or profile selections. A maximum of 5 inputs per plugin is allowed.\n\nEach input has the following properties:\n\n* `name` (required) -- the input identifier used to reference the value in args as `$INPUT_<NAME>` (uppercase)\n* `label` -- the label shown to the user in the input dialog\n* `type` (required) -- the input type: `string`, `number`, `bool`, or `dropdown`\n* `required` -- when true, the user must provide a value before the plugin can execute\n* `default` -- a default value pre-filled in the input field (must be a valid option for `dropdown`, `\"true\"`/`\"false\"` for `bool`, or a valid number for `number`)\n* `options` -- for `dropdown` type only, defines the list of available choices\n\nInput values are available in plugin args using the format `$INPUT_<NAME>` where `<NAME>` is the uppercase version of the input name.\n\n**Input Types:**\n\n| Type | Description | UI Element |\n|------|-------------|------------|\n| `string` | Free-form text input | Text field |\n| `number` | Numeric input (integers and floats) | Text field with numeric validation |\n| `bool` | Boolean toggle | Checkbox |\n| `dropdown` | Selection from predefined options | Dropdown menu |\n\n**Example:**\n\n```yaml\nplugins:\n  demo-inputs:\n    shortCut: Ctrl-Y\n    description: Demo all input types\n    scopes:\n      - po\n    command: bash\n    background: false\n    args:\n      - -c\n      - >-\n        echo \"=== Plugin input demo ===\" &&\n        echo \"\" &&\n        echo \"Pod: $NAME\" &&\n        echo \"Namespace: $NAMESPACE\" &&\n        echo \"Context: $CONTEXT\" &&\n        echo \"\" &&\n        echo \"=== Your inputs ===\" &&\n        if [ -n \"$INPUT_MESSAGE\" ]; then echo \"Message: $INPUT_MESSAGE (set)\"; else echo \"Message: (not set)\"; fi &&\n        if [ -n \"$INPUT_COUNT\" ]; then echo \"Count: $INPUT_COUNT (set)\"; else echo \"Count: (not set)\"; fi &&\n        if [ -n \"$INPUT_ENABLED\" ]; then echo \"Enabled: $INPUT_ENABLED (set)\"; else echo \"Enabled: (not set)\"; fi &&\n        if [ -n \"$INPUT_ENVIRONMENT\" ]; then echo \"Environment: $INPUT_ENVIRONMENT (set)\"; else echo \"Environment: (not set)\"; fi &&\n        echo \"\" &&\n        read -p \"Press Enter to return to k9s...\"\n    inputs:\n      - name: message\n        label: Enter a message\n        type: string\n        required: true\n        default: hello world\n      - name: count\n        label: Enter a number\n        type: number\n        required: true\n        default: 3\n      - name: enabled\n        label: Enable feature\n        type: bool\n        required: false\n        default: true\n      - name: environment\n        label: Select environment\n        type: dropdown\n        required: true\n        default: staging\n        options:\n          - development\n          - staging\n          - production\n```\n\nK9s does provide additional environment variables for you to customize your plugins arguments. Currently, the available environment variables are as follows:\n\n* `$RESOURCE_GROUP` -- the selected resource group\n* `$RESOURCE_VERSION` -- the selected resource api version\n* `$RESOURCE_NAME` -- the selected resource name\n* `$NAMESPACE` -- the selected resource namespace\n* `$NAME` -- the selected resource name\n* `$CONTAINER` -- the current container if applicable\n* `$FILTER` -- the current filter if any\n* `$KUBECONFIG` -- the KubeConfig location.\n* `$CLUSTER` the active cluster name\n* `$CONTEXT` the active context name\n* `$USER` the active user\n* `$GROUPS` the active groups\n* `$POD` while in a container view\n* `$COL-<RESOURCE_COLUMN_NAME>` use a given column name for a viewed resource. Must be prefixed by `COL-`!\n\nCurly braces can be used to embed an environment variable inside another string, or if the column name contains special characters. (e.g. `${NAME}-example` or `${COL-%CPU/L}`)\n\n### Plugin Examples\n\nDefine several plugins and host them in a single file. These can leave in the K9s root config so that they are available on any clusters. Additionally, you can define cluster/context specific plugins for your clusters of choice by adding clusterA/contextB/plugins.yaml file.\n\nThe following defines a plugin for viewing logs on a selected pod using `ctrl-l` as shortcut.\n\n```yaml\n# Define several plugins in a single file in the K9s root configuration\n# $XDG_DATA_HOME/k9s/plugins.yaml\nplugins:\n  # Defines a plugin to provide a `ctrl-l` shortcut to tail the logs while in pod view.\n  fred:\n    shortCut: Ctrl-L\n    override: false\n    overwriteOutput: false\n    confirm: false\n    dangerous: false\n    description: Pod logs\n    scopes:\n    - pods\n    command: kubectl\n    background: false\n    args:\n    - logs\n    - -f\n    - $NAME\n    - -n\n    - $NAMESPACE\n    - --context\n    - $CONTEXT\n```\n\nSimilarly you can define the plugin above in a directory using either a file per plugin or several plugins per files as follow...\n\nThe following defines two plugins namely fred and zorg.\n\n```yaml\n# Multiple plugins in a single file...\n# Note: as of v0.40.9 you can have ad-hoc plugin dirs\n# Loads plugins fred and zorg\n# $XDG_DATA_HOME/k9s/plugins/misc-plugins/blee.yaml\nfred:\n  shortCut: Shift-B\n  description: Bozo\n  scopes:\n  - deploy\n  command: bozo\n\nzorg:\n  shortCut: Shift-Z\n  description: Pod logs\n  scopes:\n  - svc\n  command: zorg\n```\n\nLastly you can define plugin snippets in their own file. The snippet will be named from the file name. In this case, we define a `bozo` plugin using a plugin snippet.\n\n```yaml\n# $XDG_DATA_HOME/k9s/plugins/schtuff/bozo.yaml\nshortCut: Shift-B\ndescription: Bozo\nscopes:\n- deploy\ncommand: bozo\n```\n\n> NOTE: This is an experimental feature! Options and layout may change in future K9s releases as this feature solidifies.\n\n---\n\n## Benchmark Your Applications\n\nK9s integrates [Hey](https://github.com/rakyll/hey) from the brilliant and super talented [Jaana Dogan](https://github.com/rakyll). `Hey` is a CLI tool to benchmark HTTP endpoints similar to AB bench. This preliminary feature currently supports benchmarking port-forwards and services (Read the paint on this is way fresh!).\n\nTo setup a port-forward, you will need to navigate to the PodView, select a pod and a container that exposes a given port. Using `SHIFT-F` a dialog comes up to allow you to specify a local port to forward. Once acknowledged, you can navigate to the PortForward view (alias `pf`) listing out your active port-forwards. Selecting a port-forward and using `CTRL-B` will run a benchmark on that HTTP endpoint. To view the results of your benchmark runs, go to the Benchmarks view (alias `be`). You should now be able to select a benchmark and view the run stats details by pressing `<ENTER>`. NOTE: Port-forwards only last for the duration of the K9s session and will be terminated upon exit.\n\nInitially, the benchmarks will run with the following defaults:\n\n* Concurrency Level: 1\n* Number of Requests: 200\n* HTTP Verb: GET\n* Path: /\n\nThe PortForward view is backed by a new K9s config file namely: `$XDG_DATA_HOME/k9s/clusters/clusterX/contextY/benchmarks.yaml`. Each cluster you connect to will have its own bench config file, containing the name of the K8s context for the cluster. Changes to this file should automatically update the PortForward view to indicate how you want to run your benchmarks.\n\nBenchmarks result reports are stored in `$XDG_STATE_HOME/k9s/clusters/clusterX/contextY`\n\nHere is a sample benchmarks.yaml configuration. Please keep in mind this file will likely change in subsequent releases!\n\n```yaml\n# This file resides in  $XDG_DATA_HOME/k9s/clusters/clusterX/contextY/benchmarks.yaml\nbenchmarks:\n  # Indicates the default concurrency and number of requests setting if a container or service rule does not match.\n  defaults:\n    # One concurrent connection\n    concurrency: 1\n    # Number of requests that will be sent to an endpoint\n    requests: 500\n  containers:\n    # Containers section allows you to configure your http container's endpoints and benchmarking settings.\n    # NOTE: the container ID syntax uses namespace/pod-name:container-name\n    default/nginx:nginx:\n      # Benchmark a container named nginx using POST HTTP verb using http://localhost:port/bozo URL and headers.\n      concurrency: 1\n      requests: 10000\n      http:\n        path: /bozo\n        method: POST\n        body:\n          {\"fred\":\"blee\"}\n        header:\n          Accept:\n            - text/html\n          Content-Type:\n            - application/json\n  services:\n    # Similarly you can Benchmark an HTTP service exposed either via NodePort, LoadBalancer types.\n    # Service ID is ns/svc-name\n    default/nginx:\n      # Set the concurrency level\n      concurrency: 5\n      # Number of requests to be sent\n      requests: 500\n      http:\n        method: GET\n        # This setting will depend on whether service is NodePort or LoadBalancer. NodePort may require vendor port tunneling setting.\n        # Set this to a node if NodePort or LB if applicable. IP or dns name.\n        host: A.B.C.D\n        path: /bumblebeetuna\n      auth:\n        user: jean-baptiste-emmanuel\n        password: Zorg!\n```\n\n---\n\n## K9s RBAC FU\n\nOn RBAC enabled clusters, you would need to give your users/groups capabilities so that they can use K9s to explore their Kubernetes cluster. K9s needs minimally read privileges at both the cluster and namespace level to display resources and metrics.\n\nThese rules below are just suggestions. You will need to customize them based on your environment policies. If you need to edit/delete resources extra Fu will be necessary.\n\n> NOTE! Cluster/Namespace access may change in the future as K9s evolves.\n> NOTE! We expect K9s to keep running even in atrophied clusters/namespaces. Please file issues if this is not the case!\n\n### Cluster RBAC scope\n\n```yaml\n---\n# K9s Reader ClusterRole\nkind: ClusterRole\napiVersion: rbac.authorization.k8s.io/v1\nmetadata:\n  name: k9s\nrules:\n  # Grants RO access to cluster resources node and namespace\n  - apiGroups: [\"\"]\n    resources: [\"nodes\", \"namespaces\"]\n    verbs: [\"get\", \"list\", \"watch\"]\n  # Grants RO access to RBAC resources\n  - apiGroups: [\"rbac.authorization.k8s.io\"]\n    resources: [\"clusterroles\", \"roles\", \"clusterrolebindings\", \"rolebindings\"]\n    verbs: [\"get\", \"list\", \"watch\"]\n  # Grants RO access to CRD resources\n  - apiGroups: [\"apiextensions.k8s.io\"]\n    resources: [\"customresourcedefinitions\"]\n    verbs: [\"get\", \"list\", \"watch\"]\n  # Grants RO access to metric server (if present)\n  - apiGroups: [\"metrics.k8s.io\"]\n    resources: [\"nodes\", \"pods\"]\n    verbs: [\"get\", \"list\", \"watch\"]\n\n---\n# Sample K9s user ClusterRoleBinding\napiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRoleBinding\nmetadata:\n  name: k9s\nsubjects:\n  - kind: User\n    name: fernand\n    apiGroup: rbac.authorization.k8s.io\nroleRef:\n  kind: ClusterRole\n  name: k9s\n  apiGroup: rbac.authorization.k8s.io\n```\n\n### Namespace RBAC scope\n\nIf your users are constrained to certain namespaces, K9s will need to following role to enable read access to namespaced resources.\n\n```yaml\n---\n# K9s Reader Role (default namespace)\nkind: Role\napiVersion: rbac.authorization.k8s.io/v1\nmetadata:\n  name: k9s\n  namespace: default\nrules:\n  # Grants RO access to most namespaced resources\n  - apiGroups: [\"\", \"apps\", \"autoscaling\", \"batch\", \"extensions\"]\n    resources: [\"*\"]\n    verbs: [\"get\", \"list\", \"watch\"]\n  # Grants RO access to metric server\n  - apiGroups: [\"metrics.k8s.io\"]\n    resources: [\"pods\", \"nodes\"]\n    verbs:\n      - get\n      - list\n      - watch\n\n---\n# Sample K9s user RoleBinding\napiVersion: rbac.authorization.k8s.io/v1\nkind: RoleBinding\nmetadata:\n  name: k9s\n  namespace: default\nsubjects:\n  - kind: User\n    name: fernand\n    apiGroup: rbac.authorization.k8s.io\nroleRef:\n  kind: Role\n  name: k9s\n  apiGroup: rbac.authorization.k8s.io\n```\n\n---\n\n## Skins\n\nExample: Dracula Skin ;)\n\n<img src=\"assets/skins/dracula.png\" alt=\"Dracula Skin\">\n\nYou can style K9s based on your own sense of look and style. Skins are YAML files, that enable a user to change the K9s presentation layer. See this repo `skins` directory for examples.\nYou can skin k9s by default by specifying a UI.skin attribute. You can also change K9s skins based on the context you are connecting too.\nIn this case, you can specify a skin field on your cluster config aka `skin: dracula` (just the name of the skin file without the extension!) and copy this repo\n`skins/dracula.yaml` to `$XDG_CONFIG_HOME/k9s/skins/` directory. You can also change the skin by setting `K9S_SKIN` in the environment, e.g. `export K9S_SKIN=\"dracula\"`.\n\nIn the case where your cluster spans several contexts, you can add a skin context configuration to your context configuration.\nThis is a collection of {context_name, skin} tuples (please see example below!)\n\nColors can be defined by name or using a hex representation. Of recent, we've added a color named `default` to indicate a transparent background color to preserve your terminal background color settings if so desired.\n\n> NOTE: This is very much an experimental feature at this time, more will be added/modified if this feature has legs so thread accordingly!\n> NOTE: Please see [K9s Skins](https://k9scli.io/topics/skins/) for a list of available colors.\n\nTo skin a specific context and provided the file `in-the-navy.yaml` is present in your skins directory.\n\n```yaml\n#  $XDG_DATA_HOME/k9s/clusters/clusterX/contextY/config.yaml\nk9s:\n  cluster: clusterX\n  skin: in-the-navy\n  readOnly: false\n  namespace:\n    active: default\n    lockFavorites: false\n    favorites:\n    - kube-system\n    - default\n  view:\n    active: po\n  featureGates:\n    nodeShell: false\n  portForwardAddress: localhost\n```\n\nYou can also specify a default skin for all contexts in the root k9s config file as so:\n\n```yaml\n#  $XDG_CONFIG_HOME/k9s/config.yaml\nk9s:\n  liveViewAutoRefresh: false\n  screenDumpDir: /tmp/dumps\n  refreshRate: 2\n  maxConnRetry: 5\n  readOnly: false\n  noExitOnCtrlC: false\n  ui:\n    enableMouse: false\n    headless: false\n    logoless: false\n    crumbsless: false\n    splashless: false\n    noIcons: false\n    invert: false\n    # Toggles reactive UI. This option provide for watching on disk artifacts changes and update the UI live  Defaults to false.\n    reactive: false\n    # By default all contexts will use the dracula skin unless explicitly overridden in the context config file.\n    skin: dracula # => assumes the file skins/dracula.yaml is present in the  $XDG_DATA_HOME/k9s/skins directory\n    defaultsToFullScreen: false\n  skipLatestRevCheck: false\n  disablePodCounting: false\n  shellPod:\n    image: busybox\n    namespace: default\n    limits:\n      cpu: 100m\n      memory: 100Mi\n  imageScans:\n    enable: false\n    exclusions:\n      namespaces: []\n      labels: {}\n  logger:\n    tail: 100\n    buffer: 5000\n    sinceSeconds: -1\n    textWrap: false\n    disableAutoscroll: false\n    columnLock: false\n    showTime: false\n  thresholds:\n    cpu:\n      critical: 90\n      warn: 70\n    memory:\n      critical: 90\n      warn: 70\n```\n\n```yaml\n# $XDG_DATA_HOME/k9s/skins/in-the-navy.yaml\n# Skin InTheNavy!\nk9s:\n  # General K9s styles\n  body:\n    fgColor: dodgerblue\n    bgColor: '#ffffff'\n    logoColor: '#0000ff'\n  # ClusterInfoView styles.\n  info:\n    fgColor: lightskyblue\n    sectionColor: steelblue\n  # Help panel styles\n  help:\n    fgColor: white\n    bgColor: black\n    keyColor: cyan\n    numKeyColor: blue\n    sectionColor: gray\n  frame:\n    # Borders styles.\n    border:\n      fgColor: dodgerblue\n      focusColor: aliceblue\n    # MenuView attributes and styles.\n    menu:\n      fgColor: darkblue\n      # Style of menu text. Supported options are \"dim\" (default), \"normal\", and \"bold\"\n      fgStyle: dim\n      keyColor: cornflowerblue\n      # Used for favorite namespaces\n      numKeyColor: cadetblue\n    # CrumbView attributes for history navigation.\n    crumbs:\n      fgColor: white\n      bgColor: steelblue\n      activeColor: skyblue\n    # Resource status and update styles\n    status:\n      newColor: '#00ff00'\n      modifyColor: powderblue\n      addColor: lightskyblue\n      errorColor: indianred\n      highlightcolor: royalblue\n      killColor: slategray\n      completedColor: gray\n    # Border title styles.\n    title:\n      fgColor: aqua\n      bgColor: white\n      highlightColor: skyblue\n      counterColor: slateblue\n      filterColor: slategray\n  views:\n    # TableView attributes.\n    table:\n      fgColor: blue\n      bgColor: darkblue\n      cursorColor: aqua\n      # Header row styles.\n      header:\n        fgColor: white\n        bgColor: darkblue\n        sorterColor: orange\n        selectedSortColumnColor: lightskyblue\n    # YAML info styles.\n    yaml:\n      keyColor: steelblue\n      colonColor: blue\n      valueColor: royalblue\n    # Logs styles.\n    logs:\n      fgColor: lightskyblue\n      bgColor: black\n      indicator:\n        fgColor: dodgerblue\n        bgColor: black\n        toggleOnColor: limegreen\n        toggleOffColor: gray\n```\n\n---\n\n## Contributors\n\nWithout the contributions from these fine folks, this project would be a total dud!\n\n<a href=\"https://github.com/derailed/k9s/graphs/contributors\">\n  <img src=\"https://contrib.rocks/image?repo=derailed/k9s\" />\n</a>\n\n---\n\n## Known Issues\n\nThis is still work in progress! If something is broken or there's a feature\nthat you want, please file an issue and if so inclined submit a PR!\n\nK9s will most likely blow up if...\n\n1. You're running older versions of Kubernetes. K9s works best on later Kubernetes versions.\n2. You don't have enough RBAC fu to manage your cluster.\n\n---\n\n## ATTA Girls/Boys!\n\nK9s sits on top of many open source projects and libraries. Our *sincere*\nappreciations to all the OSS contributors that work nights and weekends\nto make this project a reality!\n\n---\n\n## Meet The Core Team!\n\nIf you have chops in GO and K8s and would like to offer your time to help maintain and enhance this project, please reach out to me.\n\n* [Fernand Galiana](https://github.com/derailed)\n  * <img src=\"assets/mail.png\" width=\"16\" height=\"auto\" alt=\"email\"/>  fernand@imhotep.io\n  * <img src=\"assets/twitter.png\" width=\"16\" height=\"auto\" alt=\"twitter\"/> [@kitesurfer](https://twitter.com/kitesurfer?lang=en)\n\nWe always enjoy hearing from folks who benefit from our work!\n\n## Contributions Guideline\n\n* File an issue first prior to submitting a PR!\n* Ensure all exported items are properly commented\n* If applicable, submit a test suite against your PR\n\n---\n\n<img src=\"assets/imhotep_logo.png\" width=\"32\" height=\"auto\" alt=\"Imhotep\"/> &nbsp;© 2026 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.1.1.md",
    "content": "# Release v0.1.1\n\n<br/>\n\n---\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\n<br/>\n\n---\n## Change Logs\n\n+ Added config file to tracks K9s configuration ~/.k9s/config.yml\n+ Change log file location to use Go tmp dir stdlib package.\n  Check the log destination and config file location using\n    ```shell\n    k9s info\n    ```\n+ Removed 9 namespaces limitation by allowing user to manage namespaces using\n  the namespace view or the dotfile configuration.\n+ Updated keyboard navigation on log view. Up/Down, PageUp/PageDown\n+ Added configuration to manage buffer size while viewing container logs\n+ Added fail early countermeasures. Hopefully will help us figure out non starts??\n+ Beefed up CLI arguments\n+ Changed help command to just ?\n+ Changed back command to just Esc\n+ Added filtering feature to trim down viewed resources\n  Use **/**term or **Esc** to kill filtering\n\n<br/>\n\n---\n## Resolved Bugs\n\n+ [Issue 17] Multi user log usage. Added user descriptor on log files\n+ [Issue 18] Non starts due to color. Added preflight item on README.\n+ [Issue 13] ? does not do anything.\n+ [Issue 8] Don't reset selection after deletion.\n+ [Issue 1,7] Limit available namespaces. Added config file to manage top 5 namespaces\n  and also added a switch command while in the namespace resource view.\n+ [Issue 6] Sorting/filtering. Added preliminary filtering capability. Raw search\n  on table item using /filter_me command. Use Esc to turn off filtering.\n+ [Issue 5] Scrolling in log view. Added up/down/pageUp/pageDown.\n+ [Issue 3] No output when failing. Added fail early countermeasures. Hopefully\n  will give us a heads up now to track down config issues??\n"
  },
  {
    "path": "change_logs/release_0.1.10.md",
    "content": "# Release v0.1.10\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support!!\n\n---\n\n## Change Logs\n\n---\n\n## Resolved Bugs\n\n* [Issue #92](https://github.com/derailed/k9s/issues/92)\n"
  },
  {
    "path": "change_logs/release_0.1.11.md",
    "content": "# Release v0.1.11\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support!!\n\n---\n\n## Change Logs\n\n---\n\n## Resolved Bugs\n\n* [Issue #81](https://github.com/derailed/k9s/issues/81)\n* [Issue #96](https://github.com/derailed/k9s/issues/96)\n* [Issue #95](https://github.com/derailed/k9s/issues/95)\n* [Issue #93](https://github.com/derailed/k9s/issues/93)\n"
  },
  {
    "path": "change_logs/release_0.1.2.md",
    "content": "# Release v0.1.2\n\n<br/>\n\n---\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\n<br/>\n\n---\n## Change Logs\n\n+ Navigation changed! Thanks to [Teppei Fukuda](https://github.com/knqyf263) for\n  hinting about the different modes ie command vs navigation. Now in order to\n  navigate to a specific kubernetes resource you need to issue this command\n  to say see all pods (using key `>`):\n\n    ```text\n    >po<ENTER>\n    ```\n+ Similarly to filter on a given resource you can use `/` and type your filter.\n+ In both instances `<ESC>` will back you out of command mode and into navigation mode.\n\n<br/>\n\n---\n## Resolved Bugs\n\n+ [Issue #23](https://github.com/derailed/k9s/issues/23)\n+ [Issue #19](https://github.com/derailed/k9s/issues/19)\n"
  },
  {
    "path": "change_logs/release_0.1.3.md",
    "content": "# Release v0.1.3\n\n<br/>\n\n---\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support!!\n\n<br/>\n\n---\n## Change Logs\n\n<br/>\n\n+ IMPORTANT: Changed HotKeys to single chars for most non destructive operations\n  For **command** mode use the <:> key\n  For **search** mode use the </> key\n+ Revert Delete to Ctrl-D. (Sorry for the brain fart on this!)\n+ IMPORTANT! Breaking change! The K9s config has changed to handle multi-clusters.\n  If K9s does not launch, please move over .k9s/config.yml.\n+ Added Resource for ReplicaController\n+ Added auth support for cloud provider using the same auth options as kubectl\n\n---\n## Resolved Bugs\n\n+ [Issue #50](https://github.com/derailed/k9s/issues/50)\n+ [Issue #44](https://github.com/derailed/k9s/issues/44)\n+ [Issue #42](https://github.com/derailed/k9s/issues/42)\n+ [Issue #38](https://github.com/derailed/k9s/issues/38)\n+ [Issue #36](https://github.com/derailed/k9s/issues/36)\n+ [Issue #28](https://github.com/derailed/k9s/issues/28)\n+ [Issue #24](https://github.com/derailed/k9s/issues/24)\n+ [Issue #24](https://github.com/derailed/k9s/issues/3)\n"
  },
  {
    "path": "change_logs/release_0.1.4.md",
    "content": "# Release v0.1.4\n\n<br/>\n\n---\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support!!\n\n<br/>\n\n---\n## Change Logs\n\n<br/>\n\n+ IMPORTANT! Breaking change! The K9s config was changed\n  If K9s does not launch, move over .k9s/config.yml\n+ Reworked CLI args to better support contexts\n\n---\n## Resolved Bugs\n\n+ [Issue #67](https://github.com/derailed/k9s/issues/67)\n+ [Issue #65](https://github.com/derailed/k9s/issues/65)\n+ [Issue #64](https://github.com/derailed/k9s/issues/64)\n+ [Issue #60](https://github.com/derailed/k9s/issues/60)\n+ [Issue #57](https://github.com/derailed/k9s/issues/57)\n+ [Issue #56](https://github.com/derailed/k9s/issues/56)\n"
  },
  {
    "path": "change_logs/release_0.1.5.md",
    "content": "# Release v0.1.5\n\n<br/>\n\n---\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support!!\n\n<br/>\n\n---\n## Change Logs\n\n<br/>\n\n+ [Feature #54](https://github.com/derailed/k9s/issues/54)\n  Changed pod colorer to not show error status while initializing\n  Tx! [jawahars16](https://github.com/jawahars16), [jmreicha](https://github.com/jmreicha)\n\n---\n## Resolved Bugs\n\n- Fixed release version not showing up on execs\n"
  },
  {
    "path": "change_logs/release_0.1.6.md",
    "content": "# Release v0.1.6\n\n<br/>\n\n---\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support!!\n\n<br/>\n\n---\n## Change Logs\n\n<br/>\n\n+ [Feature request #43](https://github.com/derailed/k9s/issues/43) Add CronJob Manual Trigger.\n  All of this work is attributed to [dzoeteman](https://github.com/dzoeteman). Thank you!\n+ Added ability to view logs on Job resource.\n+ [Feature request #37](https://github.com/derailed/k9s/issues/37) Added Describe on resources as\n  in kubectl describe xxx\n+ NOTE! Changed alias to `job` and `cron` vs `jo` and `cjo`\n\n---\n## Resolved Bugs\n\n- Fix issue with ServiceAccounts not displaying\n"
  },
  {
    "path": "change_logs/release_0.1.7.md",
    "content": "# Release v0.1.7\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support!!\n\n---\n\n## Change Logs\n\n* [Feature request #73](https://github.com/derailed/k9s/issues/73) Add support for search/filter\n* [Feature request #48](https://github.com/derailed/k9s/issues/48) Add support to filter alias view\n* [Feature request #30](https://github.com/derailed/k9s/issues/30) Add support for init containers\n* Major cleanup + refactor. Might have introduced some instability...\n\n---\n\n## Resolved Bugs\n\n* [Issue #71](https://github.com/derailed/k9s/issues/71) K9s no longer assumes a metrics server is\n  running. Better if it is but should not prevent from viewing resources.\n* [Issue #77](https://github.com/derailed/k9s/issues/77)\n"
  },
  {
    "path": "change_logs/release_0.1.8.md",
    "content": "# Release v0.1.8\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support!!\n\n---\n\n## Change Logs\n\n---\n\n## Resolved Bugs\n\n* [Issue #79](https://github.com/derailed/k9s/issues/79)\n"
  },
  {
    "path": "change_logs/release_0.1.9.md",
    "content": "# Release v0.1.9\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support!!\n\n---\n\n## Change Logs\n\n---\n\n## Resolved Bugs\n\n* [Issue #83](https://github.com/derailed/k9s/issues/83)\n* [Issue #84](https://github.com/derailed/k9s/issues/84)\n"
  },
  {
    "path": "change_logs/release_0.10.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.10.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nFirst off, Happy 2020 to you and yours!! Best wishes for good health and good fortune!\n\nThis release represents a major overall of K9s core. It's been a long time coming and indeed a long day in the saddle. There has been many code changes and hopefully improvements from previous releases. I think some of it is better but I've probably borked a bunch of functionality in the process ;( I look to you to help me flesh out issues and bugs, so we can move on to bigger and exciting features in 2020! Please do thread lightly on this one and make sure to keep a previous release handy just in case... This was a boatload of work to make this happen, my hope is you'll enjoy some of the improvements... In any case, and as always, if you feel they're better ways or imperfections by all means please pipe in!\n\nI would also like to take this opportunity to thank all of you for your kind PRs and issues and for your support and patience with K9s. I understand this release might be a bit torked, but I will work hard to make sure we reach stability quickly in the next few drops. Thank you for your understanding!!\n\n## VatDoesDisDo?\n\nMost of the refactors are around K8s resource fetching and viewing as well as navigation changes. Based on our observations this release might load resources a bit slower than usual but should make navigation much faster once the cache is primed. We've made some improvements to be more consistent with navigation, menus and shortcuts management. We've got ride off the breadcrumbs navigation ie no more `p` to nav back. Crumbs are now just tracking a natural resource navigation ie pod -> containers -> logs and no longer commands history. Each new command will now load a brand new breadcrumb. You can press `<esc>` to nav back to the previous page. We're also introducing a new hotkeys feature, that efforts creating shortcuts to navigate to your favorite resources ie shift-0 -> view pods, shift-1 -> view deployments (See HotKey section below). I know there were many outstanding PRS (Thank you to all that I've submitted!) and given the extent of the changes, I've resolved to incorporate them in manually vs having to deal with merge conflicts.\n\n## Custom Skins Per Cluster\n\nIn this release, we've added support for skins at the cluster level. Do you want K9s to look differently based on which cluster you're connecting to? All you'll need is to name the skin file in the K9s home directory as follows `mycluster_skin.yml`. If no cluster specific skin file is found, the standard `skin.yml` file will be loaded if present. Please checkout the `skins` directory in this repo or PR me if you have cool skins you'd like to share with your fellow K9sers as they will be featured in these release notes and in the project README.\n\n## Hot(Ness)?\n\nFeeling like you want to be able to quickly switch around your favorite resources with your very own shortcut? Wouldn't it be dandy to navigate to your deployments via a shortcut vs entering a command `:deploy`? Here is what you'll need to do to add HotKeys to your K9s sessions:\n\n1. In your .k9s home directory create a file named `hotkey.yml`\n2. For example add the following to your `hotkey.yml`. You can use short names or resource name to specify a command ie same as typing it in command mode.\n\n      ```yaml\n      hotKey:\n        shift-0:\n          shortCut: Shift-0\n          description: View pods\n          command: pods\n        shift-1:\n          shortCut: Shift-1\n          description: View deployments\n          command: dp\n        shift-2:\n          shortCut: Shift-2\n          description: View services\n          command: service\n        shift-3:\n          shortCut: Shift-3\n          description: View statefulsets\n          command: statefulsets\n      ```\n\n Not feeling too `Hot`? No worried, your custom hotkeys list will be listed in the help view.`<?>`.\n\n You can choose any keyboard shortcuts that make sense to you, provided they are not part of the standard K9s shortcuts list.\n\n## PullRequests\n\n* [PR #447](https://github.com/derailed/k9s/pull/447) K9s MacPorts support. Thank you! [Nils Breunese](https://github.com/breun)\n* [PR #446](https://github.com/derailed/k9s/pull/446) Same key invert sort. Big thanks!! [James Hiew](https://github.com/jameshiew)\n* [PR #445](https://github.com/derailed/k9s/pull/445) Use `?` to toggle help. Major thanks!! [Ramz](https://github.com/ageekymonk)\n* [PR #443](https://github.com/derailed/k9s/pull/443) Hex color skin support. Great work! [Gavin Ray](https://github.com/gavinray97)\n* [PR #442](https://github.com/derailed/k9s/pull/442) Full screen/Wrap support on log view. ATTA BOY! [Shiv3](https://github.com/shiv3)\n* [PR #412](https://github.com/derailed/k9s/pull/412) Simplify cruder interface. ATTA BOY!! (as always)[Gustavo Silva Paiva](https://github.com/paivagustavo)\n* [PR #350](https://github.com/derailed/k9s/pull/350) Sanitize file name before saving. All credits to [Tuomo Syvänperä](https://github.com/syvanpera)\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #437](https://github.com/derailed/k9s/issues/437) Error when viewing cluster role on a role binding.\n* [Issue #434](https://github.com/derailed/k9s/issues/434) Same key `?` toggle help.\n* [Issue #432](https://github.com/derailed/k9s/issues/432) Add address field to port forwards.\n* [Issue #431](https://github.com/derailed/k9s/issues/431) Add LimitRange resource support.\n* [Issue #430](https://github.com/derailed/k9s/issues/430) Add HotKey support.\n* [Issue #426](https://github.com/derailed/k9s/issues/426) Address slow scroll while in table view.\n* [Issue #417](https://github.com/derailed/k9s/issues/417) Ensure code lints correctly. Thank you Gustavo!!\n* [Issue #415](https://github.com/derailed/k9s/issues/415) Add provisions to support longer clusterinfo/namespace header.\n* [Issue #408](https://github.com/derailed/k9s/issues/408) Same key toggle inverse sort.\n* [Issue #402](https://github.com/derailed/k9s/issues/402) Add `all` support to plugin scope.\n* [Issue #401](https://github.com/derailed/k9s/issues/401) Add support for custom plugins on all views.\n* [Issue #397](https://github.com/derailed/k9s/issues/397) Support HPA v2beta1 + v2beta2.\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.10.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.10.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nMaintenance release!\n\nFirst casualties... As my pappy used to say `can't cook without making a big mess`. Also reminds me of his very last words `A Truck?`...\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #450](https://github.com/derailed/k9s/issues/450) Skins work messed up the UI. Thank you [Julio H Morimoto](https://github.com/juliohm1978)!!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.10.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.10.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nMaintenance release!\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #451](https://github.com/derailed/k9s/issues/451)\n* [Issue #415](https://github.com/derailed/k9s/issues/415)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.10.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.10.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nMaintenance release!\n\nThank you all for kicking the tires on these new drops and in making sure we get back to nominal quickly. You guys ROCK!!\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #455](https://github.com/derailed/k9s/issues/455)\n* [Issue #454](https://github.com/derailed/k9s/issues/454)\n* [Issue #453](https://github.com/derailed/k9s/issues/453)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.10.4.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.10.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nMaintenance release!\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #456](https://github.com/derailed/k9s/issues/456)\n* [Issue #452](https://github.com/derailed/k9s/issues/452)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.10.5.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.10.5\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nMaintenance release!\n\nStarting the new year with a ... Bug!\n\nHopefully moving the needle a bit more in the right direction??\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #457](https://github.com/derailed/k9s/issues/457)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.10.6.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.10.6\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nMaintenance release!\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #452](https://github.com/derailed/k9s/issues/452)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.10.7.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.10.7\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nMaintenance release!\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #458](https://github.com/derailed/k9s/issues/458)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.10.8.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.10.8\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nMaintenance release!\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #460](https://github.com/derailed/k9s/issues/460)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.10.9.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.10.9\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nMaintenance release!\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #461](https://github.com/derailed/k9s/issues/461)\n* [Issue #462](https://github.com/derailed/k9s/issues/462)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.11.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.11.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_helm.png\" align=\"center\" width=\"300\" height=\"auto\"/>\n\n## Change Logs\n\n### Anyone At The Helm?\n\nK9s now offers preliminary support for Helm 3 charts! It's been a long time coming and I know a few early users had mentioned the need, but I wanted to see where Helm3 was going first. This is a preview release to see how we fair in Helm land. Besides managing your installed charts, you will be able to perform the following operations:\n\n* Uninstall a chart\n* View chart release notes\n* View deployed manifests\n\n#### How to use?\n\nSimply enter `:charts` K9s alias command to view the deployed Helm3 charts on your cluster.\n\nIf you're using Helm3 in your current clusters, please give it a rip and also pipe in for potential features/enhancements. Mind the gap here as the paint on this feature is totally fresh...\n\n### Bring Out Your Deads...\n\nThere are also a few bugs fixes from the refactor aftermath that made this drop. I know this was a bit of a brutal transition, so thank you all for your patience and for filing issues! I am hopeful that K9s will stabilize quickly so we can move on to bigger things.\n\n---\n\n## Resolved Bugs/Features\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.11.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.11.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_helm.png\" align=\"center\" width=\"300\" height=\"auto\"/>\n\nMaintenance Release!\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #466](https://github.com/derailed/k9s/issues/466)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.11.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.11.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_helm.png\" align=\"center\" width=\"300\" height=\"auto\"/>\n\nMaintenance Release!\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #469](https://github.com/derailed/k9s/issues/469)\n* [Issue #468](https://github.com/derailed/k9s/issues/468)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.11.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.11.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_helm.png\" align=\"center\" width=\"300\" height=\"auto\"/>\n\nMaintenance Release!\n\n### Speedy Gonzales?\n\nIn this drop, we took a bit of a perf pass in light of recent issues and thanks to [Chris Werner Rau](https://github.com/cwrau) pushing me and keeping me up to speed, I've digged a bit deeper and found that there might be some seemingly innocent calls that sucked a bit of cycles during K9s refreshes. Long story short, I think this drop will improve perf by a factor of ~10x in some instances. Typically the initial load will be slower but subsequent loads should be much faster. Famous last words right? Anyhow, can't really take credit for this one as the awesome [Gustavo Silva Paiva](https://github.com/paivagustavo) suggested doing this a while back, but since I was already in flight with the refactor decided to punt until back online. And here we are now...\n\nHopefully these findings will coalesce with yours?? If not, please forward bulk Prozac patches at the address below ;)\n\nThanks Chris! Was up all night trying to figure out and what was the deal with K9s and your specific clusters. Hopefully this time for sure??\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #475](https://github.com/derailed/k9s/issues/475)\n* [Issue #473](https://github.com/derailed/k9s/issues/473)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.12.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.12.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n### Searchable Logs\n\nThere has been quite a few demands for this feature. It should now be generally available in this drop. It works the same as the resource view ie `/fred`, you can also specify a fuzzy filter using `/-f blee-duh`. The paint is still fresh on that deal and not super confident that it will work nominally as I had to rework the logs to enable. So totally possible I've hosed something in the process.\n\n### APIServer Dud\n\nAt times, it could be you've lost your api server connection while K9s was up which resulted in the `K9s screen of death` or in other words a hosed terminal session ;(. K9s should now detect this condition and close out. Once again no super sure about this implementation on that deal. So if you see K9s close out under normal condition, that means I would need to go back to the drawing board.\n\n### FullScreen Logs\n\nI've been told having a flag to set fullScreen mode preference while viewing the logs would be `awesome`. Thanks [Fardin Khanjani](https://github.com/fardin01)!\nSo there is now a new K9s config flag available to set your fullscreen logs `drathers` in your .k9s/config.yml. This flag defaults to false if not set.\n\nHere is a snippet:\n\n```yaml\n# .k9s/config.yml\nk9s:\n  refreshRate: 2\n  headless: false\n  currentContext: crashandburn666\n  currentCluster: slowassnot\n  fullScreenLogs: true\n  ...\n```\n\n### K9s Slackers\n\nI've enabled a [K9s slack channel](https://join.slack.com/t/k9sers/shared_invite/enQtOTAzNTczMDYwNjc5LWJlZjRkNzE2MzgzYWM0MzRiYjZhYTE3NDc1YjNhYmM2NTk2MjUxMWNkZGMzNjJiYzEyZmJiODBmZDYzOGQ5NWM) dedicated to all K9sers. This would be a place for us to meet and discuss ideas and use cases. I'll be honest here I am not a big slack aficionado as I don't do very well with interrupt drive workflows. But I think it would be a great resource for us all.\n\nNOTE: Please be kind to each others and threat everyone with respect as I would like this to be a safe and fun place for folks to hangout. Thank you for you support and understanding!!\n\nNOTE: I'll admit my slackFU is pretty low, so if you're a slack master, feel free to advise me for best practices around setup and management, etc... Thank you!\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #484](https://github.com/derailed/k9s/issues/484)\n* [Issue #481](https://github.com/derailed/k9s/issues/481)\n* [Issue #480](https://github.com/derailed/k9s/issues/480)\n* [Issue #479](https://github.com/derailed/k9s/issues/479)\n* [Issue #477](https://github.com/derailed/k9s/issues/477)\n* [Issue #476](https://github.com/derailed/k9s/issues/476)\n* [Issue #468](https://github.com/derailed/k9s/issues/468)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.2.0.md",
    "content": "# Release v0.2.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support!!\n\n---\n\n## Change Logs\n\n+ [Feature #97](https://github.com/derailed/k9s/issues/97)\n  Changed log view to now use kubectl logs shell command.\n  There were some issues with the previous implementation with missing info and panics.\n  NOTE! User must type Ctrl-C to exit the logs and navigate back to K9s\n+ Reordered containers to show spec.containers first vs spec.initcontainers.\n+ [Feature #29](https://github.com/derailed/k9s/issues/29)\n  Side effect of #97 Log coloring if present, will now show in the terminal.\n\n---\n\n## Resolved Bugs\n\n* [Issue #99](https://github.com/derailed/k9s/issues/99)\n* [Issue #100](https://github.com/derailed/k9s/issues/100)"
  },
  {
    "path": "change_logs/release_0.2.1.md",
    "content": "# Release v0.2.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support!!\n\n---\n\n## Change Logs\n\n+ Bad call on the log view! Reverted to original and on the radar\n  for a rewrite. This will most likely introduce some\n  disturbance in the log force, but the previous solution would not\n  work for init containers and not running pods.\n+ Added live resource filters + alias view\n+ Surfaced elevator control for table view navigation\n\n---\n\n## Resolved Bugs\n"
  },
  {
    "path": "change_logs/release_0.2.2.md",
    "content": "# Release v0.2.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support!!\n\n---\n\n## Change Logs\n\n+ [Feature #98](https://github.com/derailed/k9s/issues/98) Pod view with node name.\n+ [Feature #29](https://github.com/derailed/k9s/issues/29) Support ANSI colors in logs.\n+ [Feature #105](https://github.com/derailed/k9s/issues/29) [Experimental] Add support for manual refresh.\n\n---\n\n## Resolved Bugs\n\n+ [Issue #102](https://github.com/derailed/k9s/issues/102)\n+ [Issue #104](https://github.com/derailed/k9s/issues/104)"
  },
  {
    "path": "change_logs/release_0.2.3.md",
    "content": "# Release v0.2.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support!!\n\n---\n\n## Change Logs\n\n---\n\n## Resolved Bugs\n\n+ [Issue #109](https://github.com/derailed/k9s/issues/109)"
  },
  {
    "path": "change_logs/release_0.2.4.md",
    "content": "# Release v0.2.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support!!\n\n---\n\n## Change Logs\n\n+ [Issue #87](https://github.com/derailed/k9s/issues/87) Added confirmation dialog on\n  resource deletion. Either hit <ESC> or <Cancel> button to bail out of deletion.\n\n---\n\n## Resolved Bugs\n"
  },
  {
    "path": "change_logs/release_0.2.5.md",
    "content": "# Release v0.2.5\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support!!\n\n---\n\n## Change Logs\n\n+ Added a help view to show available key bindings. Use `<?>` to access it.\n+ Alias view is now accessible via key `<a>`\n+ Pressing `<enter>` while on the namespace/context views will navigate directly to the pods view.\n+ Added resource view breadcrumbs to easily navigate in history. Use key `<p>` to navigate back.\n+ Added configuration `logBufferSize` to limit the size of the log view while viewing chatty or big logs.\n\n---\n\n## Resolved Bugs\n\n+ [Issue #116](https://github.com/derailed/k9s/issues/116)\n+ [Issue #113](https://github.com/derailed/k9s/issues/113)\n+ [Issue #111](https://github.com/derailed/k9s/issues/111)\n+ [Issue #110](https://github.com/derailed/k9s/issues/110)"
  },
  {
    "path": "change_logs/release_0.2.6.md",
    "content": "# Release v0.2.6\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support!!\n\n---\n\n## Change Logs\n\n1. Preliminary drop on sorting by resource columns\n2. Add sort by namespace, name and age for all views\n3. Add invert sort functionality on all sortable views\n4. Add sort on pod views for metrics and most other columns\n5. For all other views we will add custom sort on a per request basis\n\n\n---\n\n## Resolved Bugs\n\n+ [Issue #117](https://github.com/derailed/k9s/issues/117)\n  Was filtering out inactive ns which need to be there for all to see anyway!\n+ [Issue #59](https://github.com/derailed/k9s/issues/59)\n"
  },
  {
    "path": "change_logs/release_0.3.0.md",
    "content": "# Release v0.3.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support!!\n\n---\n\n## Change Logs\n\n1. [Feature #127](https://github.com/derailed/k9s/issues/127)\n   Added PodDisruptionBudgets initial support.\n\n---\n\n## Resolved Bugs\n\n+ [Issue #126](https://github.com/derailed/k9s/issues/126)\n+ [Issue #125](https://github.com/derailed/k9s/issues/125)\n+ [Issue #122](https://github.com/derailed/k9s/issues/122) With feeling!\n"
  },
  {
    "path": "change_logs/release_0.3.1.md",
    "content": "# Release v0.3.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support!!\n\n---\n\n## Change Logs\n\n1. Refactored a lot of code! So please watch for disturbance in the force!\n1. Changed cronjob and job aliases names to `cj` and `jo` respectively\n1. *JobView*: Added new columns\n   1. Completions\n   2. Containers\n   3. Images\n1. *NodeView* Added the following columns:\n   1. Available CPU/Mem\n   2. Capacity CPU/Mem\n1. *NodeView* Added sort fields for cpu and mem\n\n---\n\n## Resolved Bugs\n\n+ [Issue #133](https://github.com/derailed/k9s/issues/133)\n+ [Issue #132](https://github.com/derailed/k9s/issues/132)\n+ [Issue #129](https://github.com/derailed/k9s/issues/129) The easiest bug fix to date ;)\n"
  },
  {
    "path": "change_logs/release_0.3.2.md",
    "content": "# Release v0.3.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support!!\n\n---\n\n## Change Logs\n\n1. [Feature #124](https://github.com/derailed/k9s/issues/124)\n   1. *NodeView* Add current cpu/memory percentages to track current load on nodes.\n   2. *NodeView* Add requested cpu/memory percentages to track how many container\n     resources are requested on the cluster.\n   3. *NodeView* Add requested cpu/memory raw metrics\n   4. *NodeView* Add corresponding column sorters\n\n\n---\n\n## Resolved Bugs\n\n+ None\n"
  },
  {
    "path": "change_logs/release_0.3.3.md",
    "content": "# Release v0.3.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support!!\n\n---\n\n## Change Logs\n\n1. [Feature #131](https://github.com/derailed/k9s/issues/131)\n   Preliminary support for snapcraft builds ie read trying this out...\n2. [Feature #118](https://github.com/derailed/k9s/issues/118) Add arm 32/64 bit builds.\n   NOTE: will need help vetting this out as my RPi cluster is currently down.\n\n---\n\n## Resolved Bugs\n\n+ [Feature #132](https://github.com/derailed/k9s/issues/132). With feelings!\n"
  },
  {
    "path": "change_logs/release_0.4.0.md",
    "content": "# Release v0.4.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support and awesome suggestions to make K9s better!!\n\n---\n\n## Change Logs\n\n> NOTE! Lots of changes here, please report any disturbances in the force. Thank you!\n\n1. [Feature #82](https://github.com/derailed/k9s/issues/82)\n   1. Added ability to view RBAC policies while in clusterrole or role view.\n   2. The RBAC view will auto-refresh just like any K9s views hence showing live RBAC updates\n   3. RBAC view supports standard K8s verbs ie get,list,deletecollection,watch,create,patch,update,delete.\n   4. Any verbs not in this standard K8s verb list, will end up in the EXTRAS column.\n   5. For non resource URLS, we map standard REST verbs to K8s verbs ie post=create patch=update, etc.\n   6. Added initial sorts by name and group while in RBAC view.\n   7. Usage: To activate, enter command mode via `:cr` or `:ro` for clusterrole(cr)/role(ro), select a row and press `<enter>`\n   8. To bail out of the view and return to previous use `p` or `<esc>`\n2. One feature that was mentioned in the comments for the RBAC feature above Tx [faheem-cliqz](https://github.com/faheem-cliqz)! was the ability to check RBAC rules for a given user. Namely reverse RBAC lookup\n   1. Added a new view, code name *Fu* view to show all the clusterroles/roles associated with a given user.\n   2. The view also supports for checking RBAC Fu for a user, a group or an app via a serviceaccount.\n   3. To activate: Enter command mode via `:fu` followed by u|g|s:subject + `<enter>`.\n      For example: To view user *fred* Fu enter `:fu u:fred` + `<enter>` will show all clusterroles/roles and verbs associated\n      with the user *fred*\n   4. For group Fu lookup, use the same command as above and substitute `u:fred` with `g:fred`\n   5. For ServiceAccount *fred* Fu check: use `s:fred`\n3. Eliminated jitter while scrolling tables\n\n\n---\n\n## Resolved Bugs\n\n+ None\n"
  },
  {
    "path": "change_logs/release_0.4.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n\n# Release v0.4.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support and awesome suggestions to make K9s better!!\n\n---\n\n## Change Logs\n\n### o Subject View\n\n   You can now view users/groups that are bound by RBAC rules without having to type to full subject name.\n   To activate use the following command mode\n\n   ```text\n   # For users\n   :usr\n   # For groups\n   :grp\n   ```\n\n   These commands will pull all the available cluster and role bindings associated with these subject types.\n   Use select + `<enter>` to see the associated RBAC policy rules.\n   You can also filter/sort, like in any other K9s views with the added bonus of auto updates when new user/group bindings come into your clusters.\n\n   To see ServiceAccount RBAC policies, you can navigate to the serviceaccount view aka `:sa` and select + `<enter>` to view the associated policy rules.\n\n### o ~~FuView~~ is now PolicyView\n\n  The Fu command has been deprecated for pol(icy) command to see all RBAC policies available on a subject. You can use `pol` (instead of `fu`) to list out RBAC policies associated with a\n  user/group or serviceaccount.\n\n  ```text\n  # To list out all the RBAC policies associated with user `fernand`\n  :pol u:fernand\n  ```\n\n### Enter. Yes Please!\n\n  Pressing `<enter>` on most resource views will now describe the resource by default.\n\n---\n\n## Resolved Bugs\n\n+ RBAC long subject names [Issue #143](https://github.com/derailed/k9s/issues/143)\n+ Support HPA v1 [Issue #140](https://github.com/derailed/k9s/issues/140)\n  > NOTE: Describe on v1 HPA is busted just like it is when running kubectl v1.13\n  > against an older cluster.\n\n---\n\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.4.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.4.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support and awesome suggestions to make K9s better!!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n### o Decode Secrets On Demand\n\n  Secrets can now be base64 decoded to view their actual content.\n\n  In the secret view you can use `ctrl-x` to decode a selected secret. [Feature #123](https://github.com/derailed/k9s/issues/123)\n\n### o YAML Highlighter\n\n  Describe and YAML commands will now yield syntax highlighted view.\n  [Feature #142](https://github.com/derailed/k9s/issues/142)\n\n---\n\n## Resolved Bugs\n\n+ Sort by age busted [Issue #145](https://github.com/derailed/k9s/issues/145)\n+ Logs not escaped correctly [Issue #137](https://github.com/derailed/k9s/issues/137)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.4.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.4.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support and awesome suggestions to make K9s better!!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n---\n\n## Resolved Bugs\n\n+ Sort by age busted (with feeling edition!) [Issue #145](https://github.com/derailed/k9s/issues/145)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.4.4.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.4.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support and awesome suggestions to make K9s better!!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n### Exiting K9s\n\n  There are a few debates about drathers on K9s key bindings. I have caved in\n  and decided to give up my beloved 'q' for quit which will no longer be bound. As of this release quitting K9s must be done via `:q` or `ctrl-c`.\n\n### Container Logs\n\n  [Feature #147](https://github.com/derailed/k9s/issues/147). The default behavior was to pick the first available container. Which meant if the pod has an init container, the log view would choose that.\n  The view will now choose the first non init container. Most likely it\n  would be the wrong choice in pod's sidecar scenarios, but for the time\n  being showing log on one of the init containers just did not make much sense. You can still pick other containers via the menu options. We will implement a better solution for this soon...\n\n### Delete Dialog\n\n  [Feature #146](https://github.com/derailed/k9s/issues/146) Tx @dperique!\n  Pressing `<enter>` on the delete dialog would delete the resource. Now\n  `cancel` is the default button. Hence you must use `<tab>` or `->` to\n  select `OK` then press `<enter>` to delete.\n\n---\n\n## Resolved Bugs\n\n+ None\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.4.5.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.4.5\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support and awesome suggestions to make K9s better!!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n### Multi containers\n\n  There was an [issue](https://github.com/derailed/k9s/issues/135) where we ran into limitations with the container\n  selection keyboard shortcuts only allowing up to 10 containers. In this release, we've changed to a pick list vs the menu\n  to select containers for both shell and logs access. This gives K9s the ability to select up to 26 containers now. This\n  is not in any way an *encouragement* to have so many containers per pods!!\n\n### Alias View ShortCut\n\n  The change above entailed having to move the alias shortcut to `A` vs `a` as the pick list shortcuts conflicted with\n  the alias view keyboard activation.\n\n\n---\n\n## Resolved Bugs\n\n+ [Issue #152](https://github.com/derailed/k9s/issues/152)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.4.6.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.4.6\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest\nrev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support and awesome suggestions to make K9s better!!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n---\n\n## Resolved Bugs\n\n+ Node overview column ordering [Issue #153](https://github.com/derailed/k9s/issues/153)\n+ Changed foreground color on container pick list [Issue #132](https://github.com/derailed/k9s/issues/132)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.4.7.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.4.7\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support and awesome suggestions to make K9s better!!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n### Popeye Support\n\nManaging and operating a cluster is the wild is hard and getting harder.\nI've created [Popeye](https://github.com/derailed/popeye) to help with cluster sanitation and best practices. Since K9s folks are so awesome, you're getting a sneak peek! I figured why not integrate it with K9s? No need to install yet another CLI right? Provided I did not mess this up too much, you should now be able to use command mod `:popeye` to access Popeye sanitizer reports within\nK9s and scan your clusters. You can read more about it [here](https://medium.com/@fernand.galiana/k8s-clusters-oh-biff-em-popeye-637e9312963)\nand if you like so give it a clap or two ;)\n\nNOTE: In a K9s environment, if you'd like to specify a spinach config file, you must set it in your $HOME/.k9s/spinach.yml.\n\nNOTE: There is a bit more that need to be done on this integration to be complete. Please file an issue if something does not work as expected.\n\nNOTE: Popeye will run its own course and K9s is just a viewer for it, so if you'd like additional sanitation or find Popeye related issues, please tune to the corresponding repo!\n\nLet us know if you dig it? And share your before/after clusters scores!\n\n---\n\n## Resolved Bugs\n\n+ Great find! Thank you @swe-covis! Moved alias view to `Ctrl-A` [Issue #156](https://github.com/derailed/k9s/issues/156)\n+ Added toggle autoscroll via `s` key [Issue #155](https://github.com/derailed/k9s/issues/155)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.4.8.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.4.8\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try\nto mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support and awesome suggestions to make K9s better!!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n---\n\n## Resolved Bugs\n\n+ [Issue #159](https://github.com/derailed/k9s/issues/159)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.5.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.5.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support and awesome suggestions to make K9s better!!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nI am super excited about this drop of K9s. Lots of cool improvements based on K9s friends excellent feedback!\n\n\n### Popeye\n\nTurns out [Popeye](https://github.com/derailed/popeye) is in too much flux at present, thus I've decided to remove it from K9s for the time being.\n\n### ContainerView\n\nAdded a container view to list all the containers available on a given pod. On a selected pod, you can now press `<enter>` to view all of it's associated containers. Once in container view pressing `<enter>` on a selected container, will show the container logs.\n\n### Resource Traversals\n\n> Ever wanted to know where your pods originated from?\n\nFear not, K9s has got your back! Some folks have expressed desires to navigate from a deployment to its pods or see which pods are running on a given node. Whether you are starting from a Node, a Deployment, ReplicaSet, DaemonSet or StatefulSet, you can now simply `<enter>` of a selected item a view the associated pods. [Issue #149](https://github.com/derailed/k9s/issues/149)\n\n### RollingBack ReplicaSets\n\nYou can now select a ReplicaSet and rollback your Deployment to that version. Enter the command mode via `:rs` to view ReplicaSets, select the replica you want to rollback to and use `Ctrl-B` to rollback your deployment to that revision.\n\n---\n\n## Resolved Bugs\n\n+ [Issue #163](https://github.com/derailed/k9s/issues/163)\n+ [Issue #162](https://github.com/derailed/k9s/issues/162)\n+ [Issue #39](https://github.com/derailed/k9s/issues/39)\n+ [Issue #27](https://github.com/derailed/k9s/issues/27)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.5.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.5.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support and awesome suggestions to make K9s better!!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nMinor code cleanup and some display bug fixes.\n\n---\n\n## Resolved Bugs\n\n+ [Issue #168](https://github.com/derailed/k9s/issues/168)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.5.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.5.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support and awesome suggestions to make K9s better!!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n\n---\n\n## Resolved Bugs\n\n+ [Issue #171](https://github.com/derailed/k9s/issues/171)\n+ [Issue #173](https://github.com/derailed/k9s/issues/173)\n+ [Issue #174](https://github.com/derailed/k9s/issues/174)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.6.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.6.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support and awesome suggestions to make K9s better!!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n### K9s Got Skins\n\nYou can now skin K9s based on your own sense of style. Skinning, is currently limited to color variations and is still very much experimental. More details on how to achieve this is provided in the README and skins sample directory on this repo. This could be a prime opportunity for someone to contribute to this project and help us come up with some cooler looks and share with all K9s folks. Any cool skins contributed will be added and featured in this repo 🐶!\n\n### Possible instability\n\nJust spent my birthday weekend tracking down a weird synchronization issue ;( I might have introduced some instability in the process. So please thread lightly and\nplease report any weirdness. Thank you!!\n\n---\n\n## Resolved Bugs\n\n+ [Issue #169](https://github.com/derailed/k9s/issues/169)\n+ [Issue #171](https://github.com/derailed/k9s/issues/171)\n+ [Issue #172](https://github.com/derailed/k9s/issues/172)\n+ [Issue #175](https://github.com/derailed/k9s/issues/175)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.6.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.6.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support and awesome suggestions to make K9s better!!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n---\n\n## Resolved Bugs\n\n+ [Issue #171](https://github.com/derailed/k9s/issues/171) With feelings...\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.6.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.6.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support and awesome suggestions to make K9s better!!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n### Performance\n\nIn our attempt to remediate screens lock outs, it looks like K9s performance on certain clusters took a major dive. In this drop we've taken a peek at improving some of the perf issues tho much more investigating does remain. Big ATTA Boys! in effect this week to @eldada and @despairblue for kind support in helping me track down some of these issues. We're not done yet but hopefully this drop will be a bit of an improvement in the 0.6.x lineage. Thank you all for your patience and support!!\n\n---\n\n## Resolved Bugs\n\n+ [Issue #176](https://github.com/derailed/k9s/issues/171) Fingers crossed it's a better drop 🙏🐭?\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.6.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.6.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support and awesome suggestions to make K9s better!!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n### Performance... With feelings!\n\nRan thru another perf pass and hope I've pushed the needle in the right direction? K9s is now leveraging informers which I think came out of CRDs work. Our initial assessments shows numbers to μsecond updates, down from milliseconds 🎉. Hopefully the outputs are still correct as I have the tendency to make things much faster with incorrect results ;( We hope to hear back from you with a report from your clusters and assessments and brace for good news? This was a deep cycle thru K9s core and more perf will be gained, once we get a chance to vet this new strategy. I'd like to take this opportunity to thank you all for your patience and incredible kindness and support. We certainly hope this drop won't turn out to be a dud as I am fresh out of prozac patches 😩\n\n---\n\n## Resolved Bugs\n\n+ [Issue #176](https://github.com/derailed/k9s/issues/171) Fingers crossed it's a better drop 🙏🐭?\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.6.4.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.6.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support and awesome suggestions to make K9s better!!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n### Aftermath...\n\nVarious bug fixes and cleanup items.\n\n---\n\n## Resolved Bugs\n\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.6.5.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.6.5\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support and awesome suggestions to make K9s better!!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n---\n\n## Resolved Bugs\n\n+ [Issue #178](https://github.com/derailed/k9s/issues/178)\n+ [Issue #179](https://github.com/derailed/k9s/issues/179)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.6.6.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.6.6\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support and awesome suggestions to make K9s better!!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n---\n\n## Resolved Bugs\n\n+ [Issue #180](https://github.com/derailed/k9s/issues/180)\n+ [Issue #181](https://github.com/derailed/k9s/issues/181)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.6.7.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.6.7\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes!\n\nIf you've filed an issue please help me verify and close.\n\nThank you so much for your support and awesome suggestions to make K9s better!!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nThis is a maintenance release to mainly resolve outstanding issues and bugs.\n\n### Tracking Percentages\n\nAdded two new columns to track cpu/memory utilization on metrics-server enabled clusters. These apply to pod,container and node view.\n\n---\n\n## Resolved Bugs\n\n+ [Issue #192](https://github.com/derailed/k9s/issues/192)\n+ [Issue #190](https://github.com/derailed/k9s/issues/190)\n+ [Issue #189](https://github.com/derailed/k9s/issues/189)\n+ [Issue #185](https://github.com/derailed/k9s/issues/185)\n+ [Issue #171](https://github.com/derailed/k9s/issues/171)\n+ [Issue #155](https://github.com/derailed/k9s/issues/155)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.7.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.7.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as always very much appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n### Labor Day Weekend?\n\nI always seem to get this wrong... Does Labor Day weekend mean you get to work on your OSS projects all weekend?\n\nI am very excited about this drop and hopefully won't be unanimous (?) on this? 🐭\n\nFor the impatient watch this! [K9s v0.7.0 Features](https://youtu.be/83jYehwlql8)\n\n### Service Traversals\n\nProvided your K8s services are head(Full), you can now navigate to the pods that match the service selector. So you will be able to traverse Pods/Containers directly from a service just like other resources like deployment, cron, sts...\n\n### Moving Forward!\n\nIn this drop, we've added support for port-forwarding that allows you to exercise your container from your local machine. To setup a port-forward, from the Pod view drill down to a selected Pod's containers, select the container that exposes the port of interest and hit `Ctrl-F`. A dialog will popup allowing you to configure a localhost port to forward to. Once set up, K9s will take you to the new PortForward view aka `pf`. Pending your terminal feature and container setup, you should be able to pop the forwarded URL directly into your browse. On iTerm2 me think `command+click` does the trick?\n\nBig thanks and ATTABOY! in full effect all week to [Brent](https://github.com/brentco) for filing this initial issue. Please keep in mind, these port-forward babies are a bit expensive to run, so make sure you choose wisely and delete any superfluous PFs!!\n\nThis feature is still work in progress. It does require a new config file to help assist with URL configurations. As it stands, your PortForwards are in effect for the current K9s session and will be terminated on exit. Please thread lightly and checkout the README under the Benchmarking section. Your feedback on this as always, is welcome and encouraged!\n\n### Hey now!\n\nThis is one feature that I think is, pardon my french.., totally `Bitch'n`! K9s now bundles [Hey](https://github.com/rakyll/hey) CLI tool from the extremely talented Jaana Dogan of Google fame. Hey allows you to benchmark HTTP service endpoints similar to apache bench.\n\nBenchmarking is enabled via keyboard shortcuts `Ctrl-B` and `Alt-B` to activate/cancel a benchmark while in PortForward and Service view. Benchmarking a service assumes the HTTP service is exposed either as NodePort or LoadBalancer. To view your benchmarks, navigate to the new Benchmark view aka `:be<ENTER>` to list your benchmarks and runs statistics.\n\nSo you now have the ability to stretch out your cluster legs by benchmarking your pods and services and gather all kind of interesting statistics directly in K9s. Generating load on your resources will help you tune your cluster resources, exercise your auto scalers, compare Canary builds perf, etc...\n\nPlease keep in mind, this is very much a moving target at this point and will change. Ingress support will come next once we solidify the existing feature. Also checkout the README for additional configuration for this feature. With the understanding the Full Monty is coming, please help us solidify these features as these are the base ingredients to even cooler things coming down the line...\n\n> NOTE! As with anything in life `Aim small, Miss small!`. You could totally overwhelm K9s with over-zealous benchmarks and port-forwards, so please start small say C:1 N:1000, measure and go from there.\n\n---\n\n## Resolved Bugs/Features\n\n+ [Issue #198](https://github.com/derailed/k9s/issues/198)\n+ [Issue #197](https://github.com/derailed/k9s/issues/197)\n+ [Issue #195](https://github.com/derailed/k9s/issues/195) Thanks to the awesome [Sebastiaan](https://github.com/tammert). You Rock Sir!!\n+ [Issue #194](https://github.com/derailed/k9s/issues/194)\n+ [Issue #187](https://github.com/derailed/k9s/issues/187)\n+ [Issue #119](https://github.com/derailed/k9s/issues/119) Added `Ctrl-S` shortcut to dump table data as csv and log data as text.\n+ [Issue #69](https://github.com/derailed/k9s/issues/69)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.7.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.7.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as always very much appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n### AfterMath\n\nLooks like I've broken some stuff in the excitement of 0.7.0! As I ran thru the video this am, I noticed the last minute screen dumps might not be a viable feature. As [Norbert](https://github.com/ncsibra) correctly points out, in issue #187 (Thanks Norbert!!), retrieving screen dumps was a pain. So I've put together a quick ScreenDump view alias `sd` to view the screen snapshots and allows to pop your editor of choice upon selection to view the screen dump file content.\n\nNOTE: You will need to use an EDITOR env var to tell K9s which editor you want to use.\n\n```shell\n# For example...\nexport EDITOR=vim\n```\n\nThis is a quick turn around, hopefully I did not hose anything else in the process ;(\n\n---\n\n## Resolved Bugs/Features\n\n+ [Issue #200](https://github.com/derailed/k9s/issues/200)\n+ [Issue #187](https://github.com/derailed/k9s/issues/187)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.7.10.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.7.10\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as always very much appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nMeow! Looks like v0.7.9 hosed the logger ;) Sorry!!\n\n---\n\n## Resolved Bugs/Features\n\n+ [Issue #245](https://github.com/derailed/k9s/issues/245)\n+ [Issue #231](https://github.com/derailed/k9s/issues/231)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.7.11.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.7.11\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as always very much appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nMaintenance release. Just code clean up and small bug fixes.\n\n---\n\n## Resolved Bugs/Features\n\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.7.12.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.7.12\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as always very much appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nMaintenance release. Just code clean up and bug fixes.\n\n---\n\n## Resolved Bugs/Features\n\n+ [Issue #259](https://github.com/derailed/k9s/issues/259)\n+ [Issue #258](https://github.com/derailed/k9s/issues/258)\n+ [Issue #256](https://github.com/derailed/k9s/issues/256)\n+ [Issue #255](https://github.com/derailed/k9s/issues/255)\n+ [Issue #252](https://github.com/derailed/k9s/issues/252)\n+ [Issue #250](https://github.com/derailed/k9s/issues/250)\n+ [Issue #246](https://github.com/derailed/k9s/issues/246)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.7.13.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.7.13\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as always very much appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nMaintenance release bug fixes\n\n---\n\n## Resolved Bugs/Features\n\n+ [Issue #266](https://github.com/derailed/k9s/issues/266)\n+ [Issue #262](https://github.com/derailed/k9s/issues/262)\n+ [Issue #246](https://github.com/derailed/k9s/issues/246) Thank you @mcristina422!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.7.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.7.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as always very much appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n### Bug Fix Drop\n\nRemoved requirement that enforces node access. In the case RBAC rules are in effect and user does not have enough RBAC-Fu to list/watch cluster nodes.\n\n---\n\n## Resolved Bugs/Features\n\n+ [Issue #202](https://github.com/derailed/k9s/issues/202)\n\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.7.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.7.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as always very much appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n\n---\n\n## Resolved Bugs/Features\n\n+ [Issue #210](https://github.com/derailed/k9s/issues/210)\n+ [Issue #209](https://github.com/derailed/k9s/issues/209)\n+ [Issue #206](https://github.com/derailed/k9s/issues/206) Thank you @carlowouters!!\n\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.7.4.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.7.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as always very much appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nMaintenance release.\n\n---\n\n## Resolved Bugs/Features\n\n+ [Issue #211](https://github.com/derailed/k9s/issues/210)\n\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.7.5.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.7.5\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as always very much appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nRats, looks like 0.7.4 is a dud! Sorry my fault, feeling burned out ;(\nPlease upgrade to 0.7.5. Thank you for your patience and support!\n\n---\n\n## Resolved Bugs/Features\n\n+ [Issue #211](https://github.com/derailed/k9s/issues/210)\n\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.7.6.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.7.6\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as always very much appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n### MultiLogs Initial Support\n\nThis is an experimental enhancement to allow to view logs for associated resources ie view logs for all containers in a pod or view container logs for pods fronted by a service, deployment, etc... directly in K9s. We've contemplated integrating the excellent `stern` CLI for this which is more full featured than the current implementation, but decided that shelling out for logs was at this juncture not ideal. Based on your feedback, we might revisit in future releases should this feature be a total dud...\n\n### Delete Dialog\n\nThe resource delete dialog was updated to provide affordance for force and cascade deletes. This should now provide an on par behavior with the `kubectl` CLI. Cascade and force options are checkboxes, please use `<ENTER>` to toggle the flags.\n\n---\n\n## Resolved Bugs/Features\n\n+ [Feature #193](https://github.com/derailed/k9s/issues/193)\n+ [Issue #205](https://github.com/derailed/k9s/issues/205)\n+ [Issue #212](https://github.com/derailed/k9s/issues/212)\n+ [Issue #215](https://github.com/derailed/k9s/issues/215)\n+ [Issue #220](https://github.com/derailed/k9s/issues/220)\n\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.7.7.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.7.7\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as always very much appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n### Labels Filters\n\nK9s now provides an affordance to filter Kubernetes resources by label (Feature #233. Thank you [Chad Hanley](https://github.com/cchanley2003)). In order to enable filtering by labels, enter the filter mode via `/` on any resource table and enter your label filter via `-l app=fred,env=prod` + `<ENTER>`.\n\n---\n\n## Resolved Bugs/Features\n\n+ [Feature #233](https://github.com/derailed/k9s/issues/233)\n+ [Issue #232](https://github.com/derailed/k9s/issues/232)\n+ [Issue #230](https://github.com/derailed/k9s/issues/230)\n+ [Issue #229](https://github.com/derailed/k9s/issues/229)\n+ [Issue #226](https://github.com/derailed/k9s/issues/226) Thank you for the excellent PR [Yves Blusseau](https://github.com/JrCs)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.7.8.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.7.8\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as always very much appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nThis is mainly a maintenance release a few bugs were fixed.\n\n### Breaking Change!\n\nWe've changed the benchmarks and skins file formats in this release. Please take a peek at the README and sample skin files for the deltas.\n\n### RBAC Checks\n\nThere was a few issues regarding running K9s on RBAC enabled clusters. It turns out some of the permission checks were faulty. In this release, we hope these are now fixed. Please send us issues if that is not the case.\n\n---\n\n## Resolved Bugs/Features\n\n+ [Issue #242](https://github.com/derailed/k9s/issues/242)\n+ [Issue #241](https://github.com/derailed/k9s/issues/241)\n+ [Issue #201](https://github.com/derailed/k9s/issues/201)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.7.9.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.7.9\n\n## Notes\n\nThank you to all that contributed with flushing out issues with K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as always very much appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nMaintenance release a few bugs and code cleanup items.\n\n---\n\n## Resolved Bugs/Features\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.8.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.8.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nPretty excited about this drop! I am as ever humbled by all the cool comments and suggestions you guys are coming up with.\nThere are a few features that were requested that are simply excellent! Thank you all for your support, feedback and observations 👏\n\nNow that said, some features might be more or less baked, so there might be some disturbance in the force with this drop since much code churned. So please file issues or PRs 🥰 if you notice anything that no longer works as expected.\n\n### Client Update\n\nIn the mist of the next Kubernetes 1.16 drop, deprecating some old apis, we've decided to update K9s to support 1.15.1 client. We don't forsee any issues here but please make sure all is cool with this K9s drop on your clusters. If not please let us know so we can address. Thank you!!\n\n### Scaling Pods\n\nThis was feature #12 filed by [Tyler Lewis](https://github.com/alairock) many moons ago. So big thanks to Tyler!! To be honest I was on the fence with this feature as I am not a big fan of one offs when it comes to cluster management. However I think it's a great way to validate adequate HPA settings while putting your cluster under load and use K9s to figure out what reasonable number of pods might be. Now this feature was not my own implementation so all kudos on this one goes to [Nathan Piper](https://github.com/nathanpiper) for spending the time to make this a reality for all of us. So many thanks to you Nathan!!\nBy Nathan's implementation you can now leverage the `s` shortcut for scale deployments, replication controllers and statefulsets. Very cool!\n\n### FuzzBuzz!\n\nAnother enhancement request came this time from [Arthur Koziel](https://github.com/arthurk) and I think you guys will dig this one. So big thanks to Arthur for this report!! K9s now leverages a fuzzy finder to be able to search for resources. Previous implementation just used regex to locate matches. For example with this addition you can now type `promse` while in search mode `/` to locate all prometheus-server-5d5f6db7cc-XXX pods. That's so cool! Once this implementation is vetted, we will enable fuzzy searching on other views as well.\n\n### ClipBoarding\n\nThis feature comes out of [Raman Gupta](https://github.com/rocketraman) report. Thank you Raman!! This allows a K9s operator to now just hit `c` while on a resource table view to copy the currently selected resource name to the clipboard. This allows you to navigate between K9s and other tools to search, grep/etc.. thru the currently selected resource. We may want to improve on this some but the basic implementation is now available.\n\n### OldiesButGoodies?\n\nSo the initial few releases of K9s did not have any failsafe counter measures while deleting resources. So we've beefed the deletion logic to make sure you did not inadvertently blow something away by leveraging\ndialogs. This was totally a reasonable thing to do! However in case of managed pods, one may want to quickly cycle on or more pod perhaps to pickup a new image or configuration. For this purpose we've introduced an alternate deletion mechanism to delete pod under `alt-k` for kill. Thanks to my fellow frenchma [ftorto](https://github.com/ftorto) for this one ;)\n\n### HairPlugs!\n\nThis one is cool! I think this thought came about from (Markus)[https://github.com/Makusi75]. Thank you Markus!! This feature allows K9s users to now customize K9s with their own plugin commands. You will be able to add a new menu shortcut to the K9s menu and fire off a custom command on a selected resource. Some of you might be leveraging kubectl plugins and now you will be able to fire these off directly from K9s along with many other shell commands.\n\nIn order to specify a custom plugin command, you will need to modify your .k9s/config.yml file. For example here is a sample extension to list out all the pods in the `fred` namespace while in a pod or deployment view. When this plugin is available a new command `<alt-p>` will show only while in pod and deploy view.\n\n```yaml\nplugins:\n  cmd1:\n    # The menu mnemonic to trigger the command. Valid values are [a-z], Shift-[A-Z], Ctrl-[A-Z] or Alt-[A-Z]\n    # Note! Mind the cases!!!\n    shortCut: Alt-P\n    scopes: # View names are typically matching the resource shortname ie po for pod, deploy for deployment, svc for service etc... If no shortname is available use the resource name.\n    - po\n    - deploy\n    description: ViewPods # => Name to show on K9s menu\n    command: kubectl      # => The binary to use. Must be on your $PATH.\n    # Arguments on per line preceded with a dash! This will run > kubectl get pods -n fred\n    args:\n    - get\n    - pods\n    - -n\n    - fred\n```\n\nOk so this is pretty cool but what if I want to run a command to leverage the current pod name, namespace, container or other? You bet! Here is a more elaborated example. Say per Markus's report, I want to run my ksniff kubectl plugin from within K9s. So now I can hit `S` while in container view with a selected pod and sniff out incoming traffic. Here is an example plugin config for this.\n\n```yaml\nplugins:\n  ksniff:\n    # Enable `S` on the K9s menu while in container view\n    shortCut: Shift-S\n    scopes:\n    - co\n    description: Sniff\n    # NOTE! Ksniff has been installed as a kubectl extension!\n    command: kubectl\n    # Run this command in the background so that I can still do K9s stuff...\n    background: true\n    args:\n    - sniff\n    # Use a K9s env var to extract the pod name from the current view.\n    - $POD\n    - -n\n    # Use K9s current namespace\n    - $NAMESPACE\n    # Oh and pick out the container name from column 0 on that table. Nice!!\n    - -c\n    - $COL-0 # Use $COL-[0-9] to pick up the value from the desired resource table column.\n```\n\nNOTE: This is experimental and the schema/behavior WILL change in the future, so please thread lightly!\n\n### That's a wrap!\n\nWe hope you will find some of these features useful on your day to day work with K9s. We know they are now more vendors coming into this space. Hence more choices for you to assess which of these tools makes you most happy and productive. My goal is to continue improving, speeding up and stabilizing K9s. My fuel is to see folks using it, file reports, contribute and seeing that occasional ATTA BOY! (which I must say is much more rewarding to me than money or fame...).\n\nMany thanks to all of you for your time, ideas, contributions and support!!\n\n---\n\n## Resolved Bugs/Features\n\n+ [Issue #274](https://github.com/derailed/k9s/issues/274)\n+ [Issue #273](https://github.com/derailed/k9s/issues/273)\n+ [Issue #272](https://github.com/derailed/k9s/issues/272)\n+ [Issue #271](https://github.com/derailed/k9s/issues/271)\n+ [Issue #267](https://github.com/derailed/k9s/issues/267)\n+ [Issue #247](https://github.com/derailed/k9s/issues/247)\n+ [Issue #203](https://github.com/derailed/k9s/issues/203)\n+ [Issue #12](https://github.com/derailed/k9s/issues/12) Thank you Nathan!!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.8.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.8.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n### FuzzBuster!\n\nSo it looks like going all fuzzy was a mistake as we've lost some nice searchability feature with the regex counterpart. No worries tho Fuzzy is still around! The logic for searching will default to regex like all prior K9s version. To enable fuzzy logic, I figured we will use the same idea as we did with label filters using `/-lapp=bobo` but instead using `/-fpromset`\n\n### Location, Location, Location!\n\nThere was a few issues related to screen `real estate` with K9s or more specifically the lack of it! Some folks flat out decided not to use K9s just because of the ASCII Logo ;( WTF! In this drop, I'd like to introduce a new presentation mode aka `Headless`.\n\nUsing the following command you can now run K9s headless:\n\n```shell\nk9s --headless # => Launch K9s without the header rows\n```\n\nNOTE! If you forgot your K9s shortcuts already, fear not! I've also updated the help menu so `?` will remind you of all the available options.\n\nLastly if you really dig the headless mode, you can sneak an extra `headless: true` in your ./k9s/config.yml like so:\n\n```yaml\nk9s:\n  refreshRate: 2\n  headless: false\n  ...\n```\n\n### Menu Shortcuts\n\nSome folks correctly pointed out the issue with the `Alt-XXX`. Totally my bad as my external mac keyboard unlike my MBP keyboard shows `option` and `alt` as the same key. So I've added a check to make sure the correct mnemonic is displayed based on you OS. Big Thanks for the call out to Ming, Eldad, Raman and Andrew!! Hopefully it did not hose the menu options in the process... 🙏\n\n---\n\n## Resolved Bugs/Features\n\n+ [Issue #286](https://github.com/derailed/k9s/issues/286)\n+ [Issue #285](https://github.com/derailed/k9s/issues/285)\n+ [Issue #270](https://github.com/derailed/k9s/issues/270)\n+ [Issue #223](https://github.com/derailed/k9s/issues/223)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.8.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.8.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nMaintenance release.\n\nIn this quick drop, we've opted to nuke any menu shortcut using the infamous `Alt` key. This includes the new pod kill command that is now `Ctrl-K` and for the most part the column sorting shortcuts for CPU% and MEMORY%. My apologizes to all on this fiasco as it turns out I had remapped opt->alt on my local dev machine and space it while trying to offer different key mappings. Will revisit this in the future when things simmer down a bit. Thank you to all that reported on this!\n\n---\n\n## Resolved Bugs/Features\n\n+ Nuked Alt-XXX menu mnemonic [Issue #285](https://github.com/derailed/k9s/issues/285)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.8.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.8.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\n### NetworkPolicy\n\nNetworkPolicy resource is now available under the command `np` while in command mode. It behaves like other K9s resource view. You will get a bit more information in K9s vs `kubectl` as it includes information about ingress and egress rules.\n\n### Arrrggg! New CLI Argument\n\nThere is a new K9s command option available on the CLI that affords for launching K9s with a given resource. For example using `k9s -c svc` will launch K9s with a preloaded service view. You can use the same aliases as you would while in K9s to navigate a resources. For all supports resource aliases please view the `Alias View` using `Ctrl-A`.\n\n### CRDS!\n\nWe've beefed up CRD support to allow users to navigate to the CRD instances view more readily. So you can now navigate between CRD schema and CRD instances by just hitting `ENTER` while in the `crd` view.\n\n---\n\n## Resolved Bugs/Features\n\n+ CRD Navigation [Issue #295](https://github.com/derailed/k9s/issues/295)\n+ Terminal colors [Issue #294](https://github.com/derailed/k9s/issues/294)\n+ Help menu typo [Issue #291](https://github.com/derailed/k9s/issues/291)\n+ NetworkPolicy Support [Issue #289](https://github.com/derailed/k9s/issues/289)\n+ Scaling replicas start count [Issue #288](https://github.com/derailed/k9s/issues/288)\n+ CLI command arg support [Issue #283](https://github.com/derailed/k9s/issues/283)\n+ YAML screen dump support [Issue #275](https://github.com/derailed/k9s/issues/275)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.8.4.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.8.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nMaintenance release.\n\n---\n\n## Resolved Bugs/Features\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.9.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.9.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nA lots of changes here in 0.9.0!! Please watch out for potential disturbance in the force as much code changed on this drop...\n\nFigured, I'll put a quick video out for you to checkout the latest [K9s V0.9.0](https://www.youtube.com/watch?v=bxKfqumjW4I)\n\n### Support K8s 1.16\n\nAs you might have heard K8s had a big drop with 1.16 so we've added client/server support this new kubernetes release.\n\n### Alias Alas!\n\nK9s now supports standard kubernetes short name. Major shoutout to [Gustavo](https://github.com/paivagustavo) for making this painful change happen!\nWith this change is place you can now use all standard K8s short names along with defining your own. You can now define a new alias file aka `alias.yml` in your k9s home directory `$HOME/.k9s`. An alias is made up of a command and a group/version/resource aka GVR specification as follows:\n\n```yaml\nalias:\n  fred: apps/v1/deployments # Typing fred while in command mode will list out deployments\n  pp: v1/pods               # Typing pp while in command mode will list out pods\n```\n\n### Plug For Plugins\n\nAs of this release and based on some users feedback we've moved the plugin section that used to live in the main K9s configuration file out to its own file. So as of this release we've added a new file in K9s home dir called `plugin.yml`. This is where you will define/share your K9s plugins and define your own commands and menu mnemonics. Here is an example for defining a custom command to show logs.\n\n```yaml\n# plugin.yml\nplugin:\n  fred:\n    shortCut: Ctrl-L\n    description: \"Pod logs\"\n    scopes:\n    - po\n    command: /usr/local/bin/kubectl\n    background: false\n    args:\n    - logs\n    - -f\n    - $NAME\n    - -n\n    - $NAMESPACE\n    - --context\n    - $CONTEXT\n```\n\nSpecial K9s env vars you will have access to are currently for your commands or shell scripts are as follows:\n\n* NAMESPACE\n* NAME\n* CLUSTER\n* CONTEXT\n* USER\n* GROUPS\n* COL[0-9+]\n\nI will setup a plugin/alias repo so we can share these with all K9sers. Please ping me if interested in contributing/sharing your commands. Thank you!!\n\n### Aye Aye Capt'ain!!\n\nHopefully improved overall navigation...\n\n#### Real Estate\n\nThis release allows you to maximize screen real estate via 2 combos. First, the command/filter prompt is now hidden. To enter commands or filters you can type `:` or `/` to type your commands. Second, you can toggle the header using `CTRL-H`.\n\n#### Bett'a ShortCuts\n\nYou can now use commands like `svc fred` while in command mode to directly navigate to a resource in a given namespace. Likewise to switch contexts you can now enter `ctx blee` to switch out clusters.\n\n#### Sticky Filters\n\nYou can now keep filters sticky allowing you to filter a view bases on regex, fuzzy or labels and keep the filter live while switching resources. This provides for a horizontal navigation to view the various resources for a given application. Thank you so much [Nobert](https://github.com/ncsibra) for your continuous awesome feedback!!\n\n### New Resources\n\nAdded support for StorageClass, you can now view this resource and describe it directly in K9s. Major shoutout to [Oscar F](https://github.com/fridokus), zero go chops and yet managed to push this PR thru with minimal support. You Sir, blew me away. Thank you!!\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #318](https://github.com/derailed/k9s/issues/318)\n* [Issue #303](https://github.com/derailed/k9s/issues/303)\n* [Issue #301](https://github.com/derailed/k9s/issues/301)\n* [Issue #300](https://github.com/derailed/k9s/issues/300)\n* [Issue #276](https://github.com/derailed/k9s/issues/276)\n* [Issue #268](https://github.com/derailed/k9s/issues/268)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.9.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.9.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nMaintenance release\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #325](https://github.com/derailed/k9s/issues/325)\n* [Issue #326](https://github.com/derailed/k9s/issues/326)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.9.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.9.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nI am absolutely blown away by your support and excitement about K9s! As I can recall, this is the first drop since we've launched K9s\nback in January 2019 that I've seen some many external contributions and PRs. Thank you!! This is both super exciting and humbling.\n\n### Core +1\n\nAs you may have noticed, there is a new voice on the project. [Gustavo Silva Paiva](https://github.com/paivagustavo) kindly accepted to become a K9s core member. Gustavo has been following and contributing to K9s for a while now and have patiently plowed thru my code ;( Raising issues, fixing them, improving code and test coverage, he has demonstrated a genuine interest on making sure K9s is better for all of us.\n\nActually, I can say enough about Gustavo since I don't know him that well yet ;) But I can tell from my interactions with him that he is a great human being, smart, kind and consensus and hence an awesome K9s addition. Please help me in welcoming him to the K9s pac!\n\n### Breaking Bad\n\nThere was an issue with the header toggle mnemonic `Ctrl-H` and it has been changed in this release to just `h`. Thank you for the heads up [Swe Covis](https://github.com/swe-covis)!!\n\n## Merged PRs\n\n* [PR #365](https://github.com/derailed/k9s/pull/365) Fix Alias columns sorting.\n* [PR #363](https://github.com/derailed/k9s/issues/363) Change Terminated to Terminating\n* [PR #360](https://github.com/derailed/k9s/pull/360) Header toggle while typing commands\n* [PR #359](https://github.com/derailed/k9s/pull/359) Add support for CRD v1beta1\n* [PR #356](https://github.com/derailed/k9s/pull/356) Remove Object field from CRD yaml\n* [PR #347](https://github.com/derailed/k9s/pull/347) Sort node roles\n* [PR #346](https://github.com/derailed/k9s/pull/346) Optimize configmap and secret rendering\n* [PR #342](https://github.com/derailed/k9s/pull/342) Add copy YAML to clipboard\n* [PR #338](https://github.com/derailed/k9s/pull/338) Escape describe text\n* [PR #330](https://github.com/derailed/k9s/pull/330) Don't override standard K8s short names\n* [PR #324](https://github.com/derailed/k9s/pull/324) Leverage cached client to speed up K9s\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #361](https://github.com/derailed/k9s/issues/361)\n* [Issue #341](https://github.com/derailed/k9s/issues/341)\n* [Issue #335](https://github.com/derailed/k9s/issues/335)\n* [Issue #331](https://github.com/derailed/k9s/issues/331)\n* [Issue #323](https://github.com/derailed/k9s/issues/323)\n* [Issue #280](https://github.com/derailed/k9s/issues/280)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_0.9.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.9.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n## Change Logs\n\nMaintenance release\n\n## Merged PRs\n\n* [PR #385](https://github.com/derailed/k9s/pull/385) Remove debugging calls from HPA\n* [PR #384](https://github.com/derailed/k9s/issues/384) Invalidate cache when switching context\n* [PR #372](https://github.com/derailed/k9s/pull/372) Fix race when switching context\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #392](https://github.com/derailed/k9s/issues/392)\n* [Issue #389](https://github.com/derailed/k9s/issues/389)\n* [Issue #386](https://github.com/derailed/k9s/issues/386)\n* [Issue #383](https://github.com/derailed/k9s/issues/383)\n* [Issue #382](https://github.com/derailed/k9s/issues/382)\n* [Issue #336](https://github.com/derailed/k9s/issues/336) NOTE: Sticky filters have been removed for now until we have a better plan.\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2019 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.13.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.13.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n### GitHub Sponsors\n\nI'd like to personally thank the following folks for their support and efforts with this project as I know some of you have been around since it's inception almost a year ago!\n\n* [Norbert Csibra](https://github.com/ncsibra)\n* [Andrew Roth](https://github.com/RothAndrew)\n* [James Smith](https://github.com/sedders123)\n* [Daniel Koopmans](https://github.com/fsdaniel)\n\nBig thanks in full effect to you all, I am so humbled and honored by your kind actions!\n\n### Dracula Skin\n\nSince we're in the thank you phase, might as well lasso in [Josh Symonds](https://github.com/Veraticus) for contributing the `Dracula` K9s skin that is now available in this repo under the skins directory. Here is a sneak peek of what K9s looks like under that skin. I am hopeful that like minded `graphically` inclined K9sers will contribute cool skins for this project for us to share/use in our Kubernetes clusters.\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/skins/dracula.png\"/>\n\n### XRay Vision!\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_xray.png\"/>\n\nSince we've launched K9s, we've longed for a view that would display the relationships among resources. For instance, pods may reference configmaps/secrets directly via volumes or indirectly with containers referencing configmaps/secrets via say env vars. Having the ability to know which pods/deployments use a given configmap may involve some serious `kubectl` wizardry. K9s now has xray vision which allows one to view and traverse these relationships/associations as well as check for referential integrity.\n\nFor this, we are introducing a new command aka `xray`. Xray initially supports the following resources (more to come later...)\n\n1. Deployments\n2. Services\n3. StatefulSets\n4. DaemonSets\n\nTo enable cluster xray vision for deployments simply type `:xray deploy`. You can also enter the resource aliases/shortnames or use the alias `x` for `xray`. Some of the commands available in table view mode are available here ie describe, view, shell, logs, delete, etc...\n\nXray not only will tell you when a resource is considered `TOAST` ie the resource is in a bad state, but also will tell you if a dependency is actually broken via `TOAST_REF` status. For example a pod referencing a configmap that has been deleted from the cluster.\n\nXray view also supports for filtering the resources by leveraging regex, labels or fuzzy filters. This affords for getting more of an application `cross-cut` among several resources.\n\nAs it stands Xray will check for following resource dependencies:\n\n* pods\n* containers\n* configmaps\n* secrets\n* serviceaccounts\n* persistentvolumeclaims\n\nKeep in mind these can be expensive traversals and the view is eventually consistent as dependent resources will be lazy loaded.\n\nWe hope you'll find this feature useful? Keep in mind this is an initial drop and more will be coming in this area in subsequent releases. As always, your comments/suggestions are encouraged and welcomed.\n\n### Breaking Change Header Toggle\n\nIt turns out the 'h' to toggle header was a bad move as it is use by the view navigation. So we changed that shortcut to `Ctrl-h` to toggle the header expansion/collapse.\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #494](https://github.com/derailed/k9s/issues/494)\n* [Issue #490](https://github.com/derailed/k9s/issues/490)\n* [Issue #488](https://github.com/derailed/k9s/issues/488)\n* [Issue #486](https://github.com/derailed/k9s/issues/486)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.13.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.13.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n### XRay Reloaded?\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_xray.png\"/>\n\nIn the last release excitement, forgot to link the video update. Check it out! [K9s Xray](https://www.youtube.com/watch?v=qaeR2iK7U0o). Yup, got a cold... The joy of transports on the flying Petri dishes we call airplanes ;(\n\nBased on some reported issues, decided to axe the xray icons in this drop for portability sake and also added support for replicasets. So here is the official list of supported Xray resources.\n\n1. Deployments\n2. Services\n3. StatefulSets\n4. DaemonSets\n5. ReplicaSets (New!)\n\nStill work in progress... so please proceed with caution!\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #498](https://github.com/derailed/k9s/issues/498)\n* [Issue #497](https://github.com/derailed/k9s/issues/497)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.13.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.13.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n### XRay Reloaded. Part Duh!\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_xray.png\"/>\n\nFound a waffle thin issue in the Beryllium(Be) core causing K9s xray vision to only operate on one eye ;)\nShould be all betta' now...\n\nThe `xray` command now takes an **optional** third argument for the target namespace ie `:xray dp fred` will show the Xray view for deployments in the `fred` namespace.\n\nSupported resources:\n\n* Pods\n* Deployments\n* Services\n* StatefulSets\n* DaemonSets\n* ReplicaSets\n\nStill watch out for that overbite!! hence please proceed with caution...\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #500](https://github.com/derailed/k9s/issues/500)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.13.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.13.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\n### XRay Now With Lipstick?\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_xray.png\"/>\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/xray_icons.png\"/>\n\nCall me old school, but Xray without icons made me a bit sad ;( Just like any engineer would, I do fancy eye candy once in a while...\nSo I've decided to revive the xray `icon` mode for the some of us that are not stuck with what I'd like to call `Jurassic` terminals.\nTo date, there was no way to skin the Xray view, so I've added a new xray skin config section that `currently` looks like this:\n\n```yaml\n# $HOME/.k9s/skin.yml\nk9s:\n  body:\n    fgColor: dodgerblue\n    bgColor: black\n    logoColor: orange\n  ...\n  xray:\n      fgColor: blue\n      bgColor: black\n      cursorColor: aqua\n      graphicColor: darkgoldenrod\n      # NOTE! Show xray in icon mode. Defaults to false!!\n      showIcons: true\n```\n\nSo if your terminal does not support emoji's we're still cool...\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #505](https://github.com/derailed/k9s/issues/505)\n* [Issue #504](https://github.com/derailed/k9s/issues/504)\n* [Issue #503](https://github.com/derailed/k9s/issues/503)\n* [Issue #501](https://github.com/derailed/k9s/issues/501)\n* [Issue #499](https://github.com/derailed/k9s/issues/499)\n* [Issue #493](https://github.com/derailed/k9s/issues/493)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.13.4.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.13.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\n---\n\nMaintenance Release!\n\n## GH Sponsors\n\nA Big Thank You to the following folks that I've decided to dig in and give back!! 👏🙏🎊\nThank you for your gesture of kindness and for supporting K9s!! (not to mention for replenishing my liquids during oh-dark-thirty hours 🍺🍹🍸)\n\n* [w11d](https://github.com/w11d)\n* [vglen](https://github.com/vglen)\n\n## CPU/MEM Metrics\n\nA small change here based on [Benjamin](https://github.com/binarycoded) excellent PR! We've added 2 new columns for pod/container views to indicate percentages of resources request/limits if set on the containers. The columns have been renamed to represent the resources requests/limits as follows:\n\n| Name   | Description                    | Sort Keys |\n|--------|--------------------------------|-----------|\n| %CPU/R | Percentage of requested cpu    | shift-x   |\n| %MEM/R | Percentage of requested memory | shift-z   |\n| %CPU/L | Percentage of limited cpu      | ctrl-x    |\n| %MEM/L | Percentage of limited memory   | ctrl-z    |\n\n---\n\n## Resolved Bugs/Features\n\n* [Issue #507](https://github.com/derailed/k9s/issues/507) ??May be??\n* [PR #489](https://github.com/derailed/k9s/issues/489) ATTA Boy! [Benjamin](https://github.com/binarycoded)\n* [PR #491](https://github.com/derailed/k9s/issues/491) Big Thanks! [Bjoern](https://github.com/bjoernmichaelsen)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.13.5.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.13.5\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\nMaintenance Release!\n\n---\n## Resolved Bugs/Features\n\n* [Issue #507](https://github.com/derailed/k9s/issues/507)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.13.6.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.13.6\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n### GH Sponsorships\n\nWOOT!! Big Thank you in this release to [shiv3](https://github.com/shiv3) for your contributions and support for K9s!\nDuly noted and so much appreciated!!\n\n---\n\n### Bow Or Stern?\n\nSome of you had voiced wanting to enable the multi pod logger [Stern](https://github.com/wercker/stern) from the good folks at [Wercker](https://github.com/wercker). Well now you can!\n\nTo make this work the awesome [Tuomo Syvänperä](https://github.com/syvanpera) contributed a PR to enable to plug this in with K9s. Thank you Tuomo!!\nBy default the filter will be set to the currently selected pod. If you need to change the filter, simply filter the pod view to using your own regex and that's the filter K9s will use. Here is a sample plugin that defines a new K9s shortcut to launch Stern provided of course it is installed on your box...\n\n```yaml\n# K9s plugin.yml\nplugin:\n  stern:\n    shortCut: Ctrl-L\n    description: \"Logs (Stern)\"\n    scopes:\n      - pods\n    command: /usr/local/bin/stern # NOTE! Look for the command at this location.\n    background: false\n    args:\n    - --tail\n    - 50\n    - $FILTER # NOTE! Pulls the filter out of the pod view.\n    - -n\n    - $NAMESPACE\n    - --context\n    - $CONTEXT\n```\n\n---\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #507](https://github.com/derailed/k9s/issues/507)\n* [PR #510](https://github.com/derailed/k9s/pull/510) Thank you!! [Vimal Kumar](https://github.com/vimalk78)\n* [PR #340](https://github.com/derailed/k9s/pull/340) ATTA Boy! [Tuomo Syvänperä](https://github.com/syvanpera)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.13.7.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.13.7\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n### GH Sponsorships\n\nWOOT!! Big Thank you in this release to [Matthew Davis](https://github.com/mateothegreat) for your contributions and support for K9s!\n\n---\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #520](https://github.com/derailed/k9s/issues/520)\n* [Issue #518](https://github.com/derailed/k9s/issues/518)\n* [Issue #517](https://github.com/derailed/k9s/issues/517)\n* [Issue #516](https://github.com/derailed/k9s/issues/516)\n* [Issue #506](https://github.com/derailed/k9s/issues/506)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.13.8.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.13.8\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n### GH Sponsorships\n\nWOOT!! Big Thank you to [Mark Baumann](https://github.com/mtreeman) for your contributions and support for K9s!\n\n---\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #523](https://github.com/derailed/k9s/issues/523)\n* [Issue #522](https://github.com/derailed/k9s/issues/522)\n* [Issue #521](https://github.com/derailed/k9s/issues/521)\n* [PR #524](https://github.com/derailed/k9s/pull/524) Big Thanks!! [Joscha](https://github.com/joscha-alisch)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.14.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.14.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Happy Birthday K9s!!\n\n🎉🥳🎊 Doh! Almost missed it... 🎉🥳🎊\n\n Yes sir, it's been a year (already...) since K9s was first launched 🎉. I can't tell you what a year this has been 🙀. Difficult? sure. However, you guys are making this project a total gas, by your candor, kindness and for giving back via your creative issues, prs, sponsorships, slack channel help to name a few... I do think, you've all been all too quiet tho 🐭... So if K9s helps make your K8s life bett'a on a day to day basis, please reach out for your shoe-phones and dial up [@kitesurfer](https://twitter.com/kitesurfer) or write an article/blog and share it! Lastly I am so humbled by this... but we're closing on 5k stars/136k downloads in this repo, so please invite 28 of your closest friends soon...\n\nMajor Thanks to all of you for you patience and for making this project a reality to all our K8s friends! You're all redefining awesomeness!!\n\nAlso I'd like to take this opportunity to recognize and thank a few folks that have willingly volunteered their own time to track down issues and help improve K9s for all of us!!\n\n* [Gustavo Silva Paiva](https://github.com/paivagustavo)\n* [Joscha Alisch](https://github.com/joscha-alisch)\n* [Michael Christina](https://github.com/mcristina422)\n* [Bruno Meneguello](https://github.com/bkmeneguello)\n* [Tuomo Syvänperä](https://github.com/syvanpera)\n* [Oskar F](https://github.com/fridokus)\n* [Bruno Ohms](https://github.com/brunohms)\n* [IgorRamalho](https://github.com/IgorRamalho)\n* [Benjamin](https://github.com/binarycoded)\n* [Norbert Csibra](https://github.com/ncsibra)\n* [Andrew Roth](https://github.com/RothAndrew)\n* [Sgandon](https://github.com/sgandon)\n* [Chris Werner Rau](https://github.com/cwrau)\n* [Eldad Assis](https://github.com/eldada)\n* [Tobias](https://github.com/mycrEEpy)\n* [Helge Sychla](https://github.com/hsychla)\n* [Makusi75](https://github.com/Makusi75)\n* [Swe-Covis](https://github.com/swe-covis)\n* [Evgeniy Shubin](https://github.com/com30n)\n\n## Search Enabled For Describe/YAML views\n\nIn this drop we made the Describe/YAML views searchable. So you no longer need to plow thru your resource configurations and get directly to the gist of it by using the search command ie `/elvis` + `enter`. You can use the familiar keys `n` and `N` to nav back and forth to the next occurrence in a circular buffer fashion once you've reached the BOF/EOF. It's the little things in life...\n\n## And On Another Note...\n\nMore bugz...😿\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #536](https://github.com/derailed/k9s/issues/536)\n* [Issue #526](https://github.com/derailed/k9s/issues/526)\n* [Issue #464](https://github.com/derailed/k9s/issues/464)\n\n* [PR #532](https://github.com/derailed/k9s/pull/532) Thank you!! [Joscha Alisch](https://github.com/joscha-alisch)\n* [PR #525](https://github.com/derailed/k9s/pull/525) Big Thanks!! [darklore](https://github.com/darklore)\n* [PR #524](https://github.com/derailed/k9s/pull/524) Thank you (Again)!! [Joscha Alisch](https://github.com/joscha-alisch)\n* [PR #514](https://github.com/derailed/k9s/pull/514) ATTA Boy!! [Alexander F. Rødseth](https://github.com/xyproto)\n* [PR #483](https://github.com/derailed/k9s/pull/483) Thank you!! [Paul Varache](https://github.com/paulvarache)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.14.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.14.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Term Color Part Duh!\n\nSome folks had reported issues with skins and wanting to preserve their terminal background colors while in K9s. In this drop, we're introducing a new skin setting called `default` that should enable the skin to keep the original terminal background color. Here is a sample skin snippet that should achieve just that:\n\n```yaml\n# .k9s/pale_rider.yml\n\n# Styles...\nfg: &fg \"#ff00ff\"\nbg: &bg \"default\" # default keeps your current terminal window background color.\n\n# Skin...\nk9s:\n  body:\n    fgColor: *fg\n    bgColor: \"default\"\n#...\n```\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #539](https://github.com/derailed/k9s/issues/539)\n* [Issue #538](https://github.com/derailed/k9s/issues/538) Fingers crossed!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.15.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.15.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_fez.png\" align=\"center\" width=\"400\" height=\"auto\"/>\n\n## Seen This Fez Before?\n\nThe awesome and ever so smart and creative [Alex Ellis](https://github.com/alexellis) of [OpenFaas Fame](https://www.openfaas.com) fame, had pinged me when I had launched K9s to add support for OpenFaas functions. It's been a long time coming indeed, but we now have a very (VERY!) primitive integration with this very cool framework.\n\nThe current approach is to enable a few environment variables to tell K9s that you have an OpenFaas cluster available namely:\n\n```shell\nOPENFAAS_GATEWAY=http://YOUR_CLUSTER_IP:31112\nOPENFAAS_TLS_INSECURE=false\nOPENFAAS_JWT_TOKEN=YOUR_TOKEN\n```\n\nThese will tell K9s that an OpenFaas gateway is available and exposed on a given nodeport.\n\nNext you can navigate to your OpenFaas function view by entering command mode `:openfaas` or using aliases `:ofaas` or `ofa`\n\nIf functions are present in the given namespace they will be displayed here just like any other K8s resources.\n\nThe following operations are currently supported:\n\n* Describe and YAML to view function definitions (Note: currently yields same results!)\n* Enter to view all pods instances associated with the selected function\n* Delete a function\n* Editing, shelling, logs, etc... are all supported by navigating to the underlying pods\n\nKeep in mind, the paint is way fresh here and this feature could be a complete dud, but figure will give it a rinse on this drop and Alex can pipe in and helps us ironing this out.\n\n> NOTE! It's been a while since I've played with OpenFaas so if some of you are more versed in this space by all means please do land a hand so we can make this feature more awesome!\n\n## Moving Forward!\n\nA few folks had mentioned the eagerness to port-forward directly from a pod or a service. Well now you can! Port Forwarding is now available on both the pod view and services view. Note! at the end of the day, you are still port-forwarding to a container! So the port-forward dialog is a bit different for these views as there might be several container ports available now when looking at this from a pod perspective. So the first field in the dialog is a combo-box that allows one to pick their desired ports. The rest of the dialog works the same as the container port-forward dialog.\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #546](https://github.com/derailed/k9s/issues/546) BREAKING NEWS! Use `t` vs `ctrl-h` now to toggle the K9s header\n* [Issue #541](https://github.com/derailed/k9s/issues/541)\n* [Issue #227](https://github.com/derailed/k9s/issues/227)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.15.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.15.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_fez.png\" align=\"center\" width=\"400\" height=\"auto\"/>\n\n## OpenFeZ Reloaded?\n\n🙀With feelings and one less bugZ!\n\nThe awesome and ever so smart and creative [Alex Ellis](https://github.com/alexellis) of [OpenFaas Fame](https://www.openfaas.com) fame, had pinged me when I had launched K9s to add support for OpenFaas functions. It's been a long time coming indeed, but we now have a very (VERY!) primitive integration with this very cool framework.\n\nThe current approach is to enable a few environment variables to tell K9s that you have an OpenFaas cluster available namely:\n\n```shell\nOPENFAAS_GATEWAY=http://YOUR_CLUSTER_IP:31112\nOPENFAAS_TLS_INSECURE=false\nOPENFAAS_JWT_TOKEN=YOUR_TOKEN\n```\n\nThese will tell K9s that an OpenFaas gateway is available and exposed on a given nodeport.\n\nNext you can navigate to your OpenFaas function view by entering command mode `:openfaas` or using aliases `:ofaas` or `ofa`\n\nIf functions are present in the given namespace they will be displayed here just like any other K8s resources.\n\nThe following operations are currently supported:\n\n* Describe and YAML to view function definitions (Note: currently yields same results!)\n* Enter to view all pods instances associated with the selected function\n* Delete a function\n* Editing, shelling, logs, etc... are all supported by navigating to the underlying pods\n\nKeep in mind, the paint is way fresh here and this feature could be a complete dud, but figure will give it a rinse on this drop and Alex can pipe in and helps us ironing this out.\n\n> NOTE! It's been a while since I've played with OpenFaas so if some of you are more versed in this space by all means please do land a hand so we can make this feature more awesome!\n\n## Moving Forward!\n\nA few folks had mentioned the eagerness to port-forward directly from a pod or a service. Well now you can! Port Forwarding is now available on both the pod view and services view. Note! at the end of the day, you are still port-forwarding to a container! So the port-forward dialog is a bit different for these views as there might be several container ports available now when looking at this from a pod perspective. So the first field in the dialog is a combo-box that allows one to pick their desired ports. The rest of the dialog works the same as the container port-forward dialog.\n\n## Resolved Bugs/Features/PRs\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.15.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.15.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Mo PortForwards...\n\nWhile putting together the [OpenFeeZ video](https://youtu.be/7Fx4XQ2ftpM), I've noticed a few issues with port-forwards and benchmarks. While I was doing surgery on that carp, figured why not go pull a full monty on port-forwards and enable for other controller like resources such as deployments, statefulsets and daemonsets. So now you can set up port-forwards on any of these using `shift-f`. This exhibits the same mechanics as service based port-forwards ie pick a container port from pods matching the controller selector.\n\n## Resolved Bugs/Features/PRs\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.16.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.16.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_doc.png\" align=\"center\"/>\n\nThis is one of these drops that may make you wonder if you'll go from zero to hero or likely the reverse?? Will see how this goes... Please proceed with caution on this one as there could very well be much disturbances in the force...\n\nLots of code churns so could have totally hose some stuff, but like my GranPappy used to say `can't cook without making a mess!`\n\n## Going Wide?\n\nIn this drop, we've enabled a new shortcut namely `wide` as `Ctrl-w`. On table views, you will be able to see more information about the resources such as labels or others depending on the viewed resource. This mnemonic works as a toggle so you can `narrow` the view by hitting it again.\n\n## Zoom, Zoom, Zoom!\n\nWhile viewing some resources that may contain errors, sorting on columns may not achieve the results you're seeking ie `show me all resources in an error state`. We've added a new option to achieve just that aka `zoom errors` as `ctrl-z`. This works as a toggle and will unveil resources that are need of some TLC on your part ;)\n\n## Does Your Cluster Have A Pulse 💓?\n\nIn this drop, we're introducing a brand new view aka `K9s Pulses` 💓. This is a summary view listing the most salient resources in your clusters and their current states. This view tracks two main metrics ie Ok and Toast on a 5sec beat. This view affords cluster activity and failure rates. BTW this is the zero to hero deal 🙀 Hopefully you'll dig it as this was much work to put together and I personally think it's the `ducks nuts`... If you like, please give me some luving on social or via GH sponsors as batteries are running low...\n\nTo active, enter command mode by typing in `:pulse` aliases are `pu`, `pulses` or `hz`\nTo navigate thru the various pulses, you can use `tab`/`backtab` or use the menu index (just like namespaces selectors). Once on a pulse view, you can press `enter` to see the associated resource table view. Pressing `esc` will nav you back.\n\nAs I've may have mentioned before, my front-end/UX FU is weak, so I've also added a way for you to skin the charts via skins yaml to your own liking. Please see the skin section below for an example on how to skin the pulses dials. BONUS you should be able to skin K9s live! How cool is that 😻?\n\nNOTE: Pulses are very much experimental and could totally bomb on your clusters! So please thread carefully and please do report (kindly!) back.\n\n## BReaking Bad!\n\nIn this drop I've broken a few things (that I know of...), here is the list as I can recall...\n\n1. Toggle header aka `my red headed step child`. Key moved (again!) now `Ctrl-e`\n2. Skin yaml layout CHANGED! Moved table and xray sections under views and added charts section.\n\n## Skins Updates!\n\nThe skin file format CHANGE! If you are running skins with K9s, please make sure to update your skin file. If not K9s could bomb coming up!\n\nNOTE: I don't think I'll get around to update all the contributed skins in this repo `skins` dir. If you're looking for a way to help out and are UI inclined, please take a peek and make them cool!\n\n```yaml\n# my_cluster_skin.yml\n# Styles...\nforeground: &foreground \"#f8f8f2\"\nbackground: &background \"#282a36\"\ncurrent_line: &current_line \"#44475a\"\nselection: &selection \"#44475a\"\ncomment: &comment \"#6272a4\"\ncyan: &cyan \"#8be9fd\"\ngreen: &green \"#50fa7b\"\norange: &orange \"#ffb86c\"\npink: &pink \"#ff79c6\"\npurple: &purple \"#bd93f9\"\nred: &red \"#ff5555\"\nyellow: &yellow \"#f1fa8c\"\n\n# Skin...\nk9s:\n  # General K9s styles\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *purple\n  # ClusterInfoView styles.\n  info:\n    fgColor: *pink\n    sectionColor: *foreground\n  frame:\n    # Borders styles.\n    border:\n      fgColor: *selection\n      focusColor: *current_line\n    menu:\n      fgColor: *foreground\n      keyColor: *pink\n      # Used for favorite namespaces\n      numKeyColor: *purple\n    # CrumbView attributes for history navigation.\n    crumbs:\n      fgColor: *foreground\n      bgColor: *current_line\n      activeColor: *current_line\n    # Resource status and update styles\n    status:\n      newColor: *cyan\n      modifyColor: *purple\n      addColor: *green\n      errorColor: *red\n      highlightcolor: *orange\n      killColor: *comment\n      completedColor: *comment\n    # Border title styles.\n    title:\n      fgColor: *foreground\n      bgColor: *current_line\n      highlightColor: *orange\n      counterColor: *purple\n      filterColor: *pink\n  views:\n    charts:\n      bgColor: *background\n      dialBgColor: \"#0A2239\"\n      chartBgColor: \"#0A2239\"\n      defaultDialColors:\n        - \"#1E3888\"\n        - \"#820101\"\n      defaultChartColors:\n        - \"#1E3888\"\n        - \"#820101\"\n      resourceColors:\n        batch/v1/jobs:\n          - \"#5D737E\"\n          - \"#820101\"\n        v1/persistentvolumes:\n          - \"#3E554A\"\n          - \"#820101\"\n        cpu:\n          - \"#6EA4BF\"\n          - \"#820101\"\n        mem:\n          - \"#17505B\"\n          - \"#820101\"\n        v1/events:\n          - \"#073B3A\"\n          - \"#820101\"\n        v1/pods:\n          - \"#487FFF\"\n          - \"#820101\"\n    # TableView attributes.\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *current_line\n      # Header row styles.\n      header:\n        fgColor: *foreground\n        bgColor: *background\n        sorterColor: *cyan\n    # Xray view attributes.\n    xray:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *current_line\n      graphicColor: *purple\n      showIcons: true\n    # YAML info styles.\n    yaml:\n      keyColor: *pink\n      colonColor: *purple\n      valueColor: *foreground\n    # Logs styles.\n    logs:\n      fgColor: *foreground\n      bgColor: *background\n```\n\n## Resolved Bugs/Features/PRs\n\n- [Issue #557](https://github.com/derailed/k9s/issues/557)\n- [Issue #555](https://github.com/derailed/k9s/issues/555)\n- [Issue #554](https://github.com/derailed/k9s/issues/554)\n- [Issue #553](https://github.com/derailed/k9s/issues/553)\n- [Issue #552](https://github.com/derailed/k9s/issues/552)\n- [Issue #551](https://github.com/derailed/k9s/issues/551)\n- [Issue #549](https://github.com/derailed/k9s/issues/549) A start with pulses...\n- [Issue #540](https://github.com/derailed/k9s/issues/540)\n- [Issue #421](https://github.com/derailed/k9s/issues/421)\n- [Issue #351](https://github.com/derailed/k9s/issues/351) Solved by Pulses?\n- [Issue #25](https://github.com/derailed/k9s/issues/25) Pulses? Oldie but goodie!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.16.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.16.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\nMaintenance Release!\n\n## Resolved Bugs/Features/PRs\n\n- [Issue #561](https://github.com/derailed/k9s/issues/561)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.17.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.17.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/beach.png\" align=\"center\"/>\n\n## Custom Columns? Yes Please!!\n\n[SneakCast v0.17.0 on The Beach! - Yup! sound is sucking but what a setting!](https://youtu.be/7S33CNLAofk)\n\nIn this drop I've reworked the rendering engine to provide for custom columns support. Now, you should be able to not only tell K9s which columns you would like to display but also which order they should be in.\n\nTo surface this feature, you will need to create a new configuration file, namely `$HOME/.k9s/views.yml`. This file leverages GVR (Group/Version/Resource) to configure the associated table view columns. If no GVR is found for a view the default rendering will take over (ie what we have now). Going wide will add all the remaining columns that are available on the given resource after your custom columns. To boot, you can edit your views config file and tune your resources views live!\n\n> NOTE: This is experimental and will most likely change as we iron this out!\n\nHere is a sample views configuration that customize a pods and services views.\n\n```yaml\n# $HOME/.k9s/views.yml\nk9s:\n  views:\n    v1/pods:\n      columns:\n        - AGE\n        - NAMESPACE\n        - NAME\n        - IP\n        - NODE\n        - STATUS\n        - READY\n    v1/services:\n      columns:\n        - AGE\n        - NAMESPACE\n        - NAME\n        - TYPE\n        - CLUSTER-IP\n```\n\n## Resolved Bugs/Features/PRs\n\n- [Issue #581](https://github.com/derailed/k9s/issues/581)\n- [Issue #576](https://github.com/derailed/k9s/issues/576)\n- [Issue #574](https://github.com/derailed/k9s/issues/574)\n- [Issue #573](https://github.com/derailed/k9s/issues/573)\n- [Issue #571](https://github.com/derailed/k9s/issues/571)\n- [Issue #566](https://github.com/derailed/k9s/issues/566)\n- [Issue #563](https://github.com/derailed/k9s/issues/563)\n- [Issue #562](https://github.com/derailed/k9s/issues/562)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.17.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.17.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\nMaintenance Release!\n\n## Resolved Bugs/Features/PRs\n\n- [Issue #584](https://github.com/derailed/k9s/issues/584)\n- [Issue #583](https://github.com/derailed/k9s/issues/583)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.17.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.17.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\nMaintenance Release!\n\n## Resolved Bugs/Features/PRs\n\n- [Issue #592](https://github.com/derailed/k9s/issues/592)\n- [Issue #591](https://github.com/derailed/k9s/issues/591)\n- [Issue #590](https://github.com/derailed/k9s/issues/590)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.17.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.17.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\nMaintenance Release!\n\n- Reworked Pulses view counters and layout.\n- Switching context now will take you to that context last view if available vs the pod view.\n- Reworked info/version layout.\n\n## Resolved Bugs/Features/PRs\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.17.4.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.17.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please consider sponsoring 👆us or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/story/this_is_fine_300.png\" align=\"center\" width=\"500\" height=\"auto\"/>\n\n## Pulses Part Duh!\n\nIn this drop, we've updated pulses to now show used/allocatable resources for cpu and mem as recommended by the awesome and kind [Eldad Assis](https://github.com/eldada)! We've also added the concept of threshold to alert you when things in your clusters are going south. These currently come in the shape of cpu and mem thresholds. They are set at the cluster level. K9s will now let you know when these limits are reached or surpassed. As it stands, the k9s logo will change color and a flash message will appear to let you know which resource threshold was exceeded. Once the load subsumes the logo/flash will return to their original states.\n\nIn order to override the default thresholds (cpu/mem: 80% ), you will need to modify your `$HOME/.k9s/config.yml` using the new config section named `thresholds` as follows:\n\n```yaml\n# $HOME/.k9s/config.yml\nk9s:\n  refreshRate: 2\n  headless: false\n  ...\n  # Specify resources thresholds percentages\n  thresholds:\n    cpu:    80 # default is 80\n    memory: 55 # default is 80\n  ...\n```\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/story/pulses_tripped.png\" align=\"center\" width=\"500\" height=\"auto\"/>\n\n## Resolved Bugs/Features/PRs\n\n- [Issue #596](https://github.com/derailed/k9s/issues/596)\n- [Issue #593](https://github.com/derailed/k9s/issues/593)\n- [Issue #560](https://github.com/derailed/k9s/issues/560)\n  - NOTE!! All credits here goes to [Bruno Meneguello](https://github.com/bkmeneguello) and [Michael Cristina](https://github.com/mcristina422) for making this possible in K9s!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.17.5.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.17.5\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please consider sponsoring 👆us or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/story/this_is_fine_300.png\" align=\"center\" width=\"500\" height=\"auto\"/>\n\n## Thresholds Reloaded!\n\nIn the previous k9s release, we've introduced the notion of thresholds to provide with an alert mechanism when either the cpu or memory goes high on your clusters. Looking at the current solution, we felt we needed a bit more granularity in the severity levels thanks to [Eldad Assis](https://github.com/eldada) feedback on this one! So here is the new configuration for cluster thresholds. Please keep in mind this feature is still in flux!\n\n```yaml\n# $HOME/.k9s/config.yml\nk9s:\n  refreshRate: 2\n  headless: false\n  ...\n  # Specify resources thresholds in percent - defaults: critical=90, warn=70\n  thresholds:\n    cpu:\n      critical: 85\n      warn: 75\n    memory:\n      critical: 80\n      warn: 70\n  ...\n```\n\n## Resolved Bugs/Features/PRs\n\n- [Issue #604](https://github.com/derailed/k9s/issues/604)\n- [Issue #601](https://github.com/derailed/k9s/issues/601) Thank you [Christian Vent](https://github.com/christian-vent)\n- [Issue #598](https://github.com/derailed/k9s/issues/598)  `Ctrl-l` will now trigger the benchmarking toggle!\n- [Issue #593](https://github.com/derailed/k9s/issues/593)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.17.6.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.17.6\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please consider sponsoring 👆us or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Get A Rope!\n\nThis was in the backlogs for a while, so I've decided to give it a bit of TLC. Thank you [Mitchell Maler](https://github.com/mitchellmaler) for this issue!\n\n🏝Feeling your clusters could use a bit of spring cleaning 🧽🧼?\nAs of this drop, you can now perform direct cluster nodes maintenance by leveraging cordon `c`, uncordon `u` and drain `d` while in node view! Each operation comes with a dialog to either configure the options or confirm the operation. You dig?\n\n## Resolved Bugs/Features/PRs\n\n- [Issue #612](https://github.com/derailed/k9s/issues/612)\n- [Issue #608](https://github.com/derailed/k9s/issues/608)\n- [Issue #606](https://github.com/derailed/k9s/issues/606)\n- [Issue #237](https://github.com/derailed/k9s/issues/237)\n- [PR #607](https://github.com/derailed/k9s/pull/607) ATTA Boy! [Jeff Widman](https://github.com/jeffwidman)\n- [PR #570](https://github.com/derailed/k9s/pull/570) Thank you [Ludovico Rosso](https://github.com/ludusrusso)!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.17.7.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.17.7\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please consider sponsoring 👆us or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## 🙀(PLUGIN-19)\n\n[Br]eaking [Ba]d on K9s plugins! In previous releases, we used the COL<INDEX> semantic to reference view column data in the plugin extensions. In this drop, we've axed this in favor of column name vs column index. This makes K9s plugin more readable and usable. Also, in light of custom columns, this old semantic just did not jive to well. To boot, all columns available on the viewed resource, regardless of display preferences or order are now free game to plugin authors. So for folks currently leveraging K9s plugins, this drop will break you I am hopeful you guys dig this approach betta'??\n\nHere is a sample plugin file that highlights the new functionality. Please see the updated docs for additional information!\n\n```yaml\nplugin:\n  toggleCronJob:\n    shortCut: Ctrl-T\n    scopes:\n      - cj\n    description: Suspend/Resume\n    command: kubectl\n    background: true\n    args:\n      - patch\n      - cronjobs\n      - $NAME\n      - -n\n      - $NAMESPACE\n      - --context\n      - $CONTEXT\n      - -p\n      - '{\"spec\" : {\"suspend\" : $!COL-SUSPEND }}' # => Used to be COL3!\n```\n\n## Resolved Bugs/Features/PRs\n\n- [Issue #616](https://github.com/derailed/k9s/issues/616)\n- [Issue #615](https://github.com/derailed/k9s/issues/615)\n- [Issue #614](https://github.com/derailed/k9s/issues/614)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.18.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.18.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please consider sponsoring 👆us or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## GH Sponsors\n\nBig `ThankYou` to the following folks that I've decided to dig in and give back!! 👏🙏🎊\nThank you for your gesture of kindness and for supporting K9s!!\n\n* [Bob Johnson](https://github.com/bbobjohnson)\n* [Poundex](https://github.com/Poundex)\n* [thllxb](https://github.com/thllxb)\n\nIf you've contributed $25 or more please reach out to me on slack with your earth coordinates so I can send you your K9s swags!\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/shirts/k9s_front.png\" align=\"center\" width=\"auto\" height=\"100\"/>\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/shirts/k9s_back.png\" align=\"center\" width=\"auto\" height=\"100\"/>\n\nNOTE: I am one not to pressure folks into giving. However, it does make me sad to see postings out there with clear indications that K9s is being used and yet zero mentions of the web site nor this repo. K9s marketing budget relies entirely on word of mouth and is not pimped out by big corps. So if you publish your work and leverage K9s, please give us a shoutout or at the very least reference this repo or website!\n\n---\n\n## AutoSuggestions\n\nK9s command mode now provides for auto complete. Suggestions are pulled from available kubernetes resources and custom aliases. The command mode supports the following keyboard triggers:\n\n| Key                 | Description                              |\n|---------------------|------------------------------------------|\n| ⬆️ ⬇️               | Navigate up or down thru the suggestions |\n| `Ctrl-w`, `Ctrl-u`  | Clear out the command                    |\n| `Tab`, `Ctrl-f`, ➡️ | Accept the suggestion                    |\n\n## Logs Revisited\n\nBreaking Change! This drop changes how logs are viewed and configured. The log view now support for pulling logs based on the log timeline current settings are: all, 1m, 5m, 15m and 1h. The following log configuration is in effect as of this drop:\n\n```yaml\n# $HOME/.k9s/config.yml\nk9s:\n  refreshRate: 2\n  readOnly: false\n  # NOTE: New logger configuration!\n  logger:\n    tail:          200 # Tail the last 100 lines. Default 100\n    buffer:       5000 # Max number of lines displayed in the view. Default 1000\n    sinceSeconds:  900 # Displays the last x seconds from the logs timeline. Default 5m\n  ...\n```\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #628](https://github.com/derailed/k9s/issues/628)\n* [Issue #623](https://github.com/derailed/k9s/issues/623)\n* [Issue #622](https://github.com/derailed/k9s/issues/622)\n* [Issue #565](https://github.com/derailed/k9s/issues/565)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.18.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.18.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please consider sponsoring 👆us or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\nMaintenance Release!\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #632](https://github.com/derailed/k9s/issues/632)\n* [Issue #631](https://github.com/derailed/k9s/issues/631)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.19.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.19.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please consider sponsoring 👆us or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## A Word From Our Sponsors...\n\nIt makes me always very happy to hear folks are digging this effort and using K9s daily! If you feel this way please tell us and consider joining our [sponsorship](https://github.com/sponsors/derailed) program.\n\nBig Thank You! to [hornbech](https://github.com/hornbech) for joining our sponsors!\n\n## K8s v1.18.0 Support\n\nAs you might have heard, the good Kubernetes folks just dropped some big features in this new release. ATTA Girls/Boys!! We've (painfully) updated K9s to now link with the latest and greatest apis. Likely more work will need to take place here as I am still trying to catch up with the latest enhancements. This is great to see and excellent for all our Kubernetes friends!\n\n## Oh Biffs'em And Buffs'em Popeye!\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_popeye.png\" align=\"center\" width=\"400\" height=\"auto\"/>\n\nAs you may know, I am the author of [Popeye](https://popeyecli.io) a Kubernetes cluster linter/sanitizer. Popeye scans your clusters live and reports potential issues, things like: referential integrity, misconfiguration, resource usage, etc...\nIn this drop, we've integrated K9s and Popeye to produce what I believe is a killer combo. Not only can you manage/observe your cluster resources in the wild, but you can now assert that your resources are indeed cool and potentially get rid of dead weights that might add up to your monthly cloud service bills. How cool is that?\n\nIn order to run your sanitization and produce reports, you can enter a new command `:popeye`. Once your cluster sanitization is complete, you can use familiar keyboard shortcuts to sort columns and view the sanitization reports by pressing `enter` on a given resource linter. Popeye also supports a configuration file namely `spinach.yml`, this file provides for customizing what resources get scanned as well as setting different severity levels to your own company policies. Please read the Popeye docs on how to customize your reports. The spinach.yml file will be read from K9s home directory `$HOME/.k9s/MY_CLUSTER_CONTEXT_NAME_spinach.yml`\n\nNOTE! This is very much still experimental, so you may experience some disturbances in the force! And remember PRs are always open ;)\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/popeye/sanitizers.png\" align=\"center\" width=\"400\" height=\"auto\"/>\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/popeye/report.png\" align=\"center\" width=\"400\" height=\"auto\"/>\n\n## Command History Support\n\nK9s now supports for command history. Entering command mode via `:` you can now up/down arrow to navigate thru your command history. Pressing `tab` or `ctrl-e` or `->` will activate the selected command upon `enter`.\n\n## K9s Icons\n\nSome terminals often don't offer icon support. In this release there is a new option `noIcons` available to enable/disable K9s icons. By default this option is set `false`. You can now set your icon preference in the K9s config file as follows:\n\n```yaml\n# $HOME/.k9s/config.yml\nk9s:\n  refreshRate: 2\n  headless: false\n  readOnly: false\n  noIcons: true  # Enable/Disable K9s icons display.\n```\n\n## Videos!\n\n* [video v0.19.0](https://www.youtube.com/watch?v=kj-WverKZ24)\n* [video v0.18.0](https://www.youtube.com/watch?v=zMnD5e53yRw)\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #647](https://github.com/derailed/k9s/issues/647)\n* [Issue #645](https://github.com/derailed/k9s/issues/645)\n* [Issue #640](https://github.com/derailed/k9s/issues/640)\n* [Issue #639](https://github.com/derailed/k9s/issues/639)\n* [Issue #635](https://github.com/derailed/k9s/issues/635)\n* [Issue #634](https://github.com/derailed/k9s/issues/634) Thank you!! [David Němec](https://github.com/davidnemec)\n* [Issue #626](https://github.com/derailed/k9s/issues/626)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.19.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.19.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please consider sponsoring 👆us or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## A Word From Our Sponsors...\n\nIt makes me always very happy to hear folks are digging this effort and using K9s daily! If you feel this way please tell us and consider joining our [sponsorship](https://github.com/sponsors/derailed) program.\n\nBig Thank You! to [Azar](https://github.com/azarudeena) for joining our sponsors!\n\nMaintenance Release!\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #649](https://github.com/derailed/k9s/issues/649)\n* [PR #638](https://github.com/derailed/k9s/pull/638) Thank you! [Shang Yuanchun](https://github.com/ideal)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.19.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.19.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, please consider sponsoring 👆us or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## A Word From Our Sponsors...\n\nIt makes me always very happy to hear folks are digging this effort and using K9s daily! If you feel this way please tell us and consider joining our [sponsorship](https://github.com/sponsors/derailed) program.\n\nBig Thank You! to the following folks for joining our program:\n\n* [Nick Hobart](https://github.com/nwhobart)\n* [Shopeonarope](https://github.com/shopeonarope)\n\nMaintenance Release!\n\nNOTE! During K9s update to support the latest version of Kubernetes (v1.18), K9s Helm charts support took one for the team ;( At this time Helm as yet to be released k8s v1.18 support. We will track for updates and enable this feature once HelmV3 releases it.\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #665](https://github.com/derailed/k9s/issues/665)\n* [Issue #662](https://github.com/derailed/k9s/issues/662)\n* [PR #660](https://github.com/derailed/k9s/pull/660) Thank you! [Tomáš Pospíšek](https://github.com/tpo)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.19.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.19.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Look Who Is Back?\n\nThanks to the good Helm folks, we're now back on par with the Helm charts support feature. As you may recall, when we've updated to K8s v1.18, the Helm feature took one for the team ;( as they had yet to upgrade to the latest k8s rev. So K9s Helm chart feature is back in this drop! On that note, we've added new aliases to allow you to view your currently installed Helm charts aka `helm` | `hm` | `chart` | `charts`.\n\n## Boh-Bye Windows 386!\n\nAs of this drop, I've decided to axe Windows 386 support. Our good friend [Guy Barrette](https://github.com/guybarrette) reported K9s Windows-386 binary is tripping his virus scanner. After double checking my installed SHAs/binaries/dependencies/etc... and performing vulnerability scans on various win-i386 K9s binaries, I just could not figure out which dependencies are causing the exec to bomb on the scans??\n\nNote: This does not necessary entails that there is a deliberate or malicious intent with the software, but likely a false positive thrown by the Windows virus scanner. This has been [reported](https://golang.org/doc/faq#virus) with other GO binaries on windows as well ;(\n\nThat said, I've repeatedly scanned the K9s Windows-x64 and ended up with a clean bill of health on every single scans. So I've decided to drop the 386 windows support for the time being. If that causes you some grief, please land a hand as I am fresh out of ideas...\n\n## And Now For Something A Bit More... Controversial?\n\nThere has been a lot of requests for K9s to support shelling directly into cluster nodes. I was resisting the temptation to support this useful feature as depending on your cluster hosting solution, this involved less than ideal solutions. My clusters are provisioned in a multitude of platforms ranging from bare metal to cloud vendor self/managed hosting. I wanted the same experience shelling into an GKE/AWS node as a local KiND cluster node. To this end, we've opted to experimentally support shelling into nodes using the following approach:\n\n1. While in the Node view, we are introducing a new `s` mnemonic to shell into nodes on your cluster.\n2. K9s will spin up a `k9s-shell` pod in the `default` namespace with an official Busybox container running in `privileged` mode. This may require extra RBAC and PSPs (This will need Docs!)\n3. Once shelled-in, you can poke around any of your nodes.\n4. Upon exiting the node shell, K9s will automatically delete the `k9s-shell` pod for that node.\n\nThis feature is `OPT-IN` only ie you will need to manually enable the feature gate to make this functionality available to K9s on a specific cluster as follows:\n\n```yaml\n# $HOME/.k9s/config.yml\nk9s:\n  ...\n  clusters:\n    fred:\n      namespace:\n        active: \"default\"\n        favorites:\n        - default\n      view:\n        active: po\n      featureGates:\n        nodeShell: true # Defaults to false!\n```\n\nPlease let us know if you dig this feature? This very much experimental and we're open to your suggestions. Thank you!\n\n## New Sheriff In Town K9S_EDITOR\n\nAs you may know K9s currently uses your `EDITOR` env var to launch an editor while editing a k8s resource or viewing a screen dump or a performance benchmark. So folks voiced they are using some editors that require different CLI args when editing k8s resources vs files on disk. In this drop, we're introducing a new env var `K9S_EDITOR` to provide an affordance to deal with these discrepancies. If you are using emacs/vi/nano no action should be required. K9s will now check for `K9S_EDITOR` existence to view K9s artifacts such as screen_dumps. K9s still honors `KUBE_EDITOR` or `EDITOR` for K8s resource edits. K9s will fallback to the `EDITOR` env var if `K9S_EDITOR` is not set.\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #669](https://github.com/derailed/k9s/issues/669)\n* [Issue #677](https://github.com/derailed/k9s/issues/677)\n* [Issue #673](https://github.com/derailed/k9s/issues/673)\n* [Issue #671](https://github.com/derailed/k9s/issues/671)\n* [Issue #670](https://github.com/derailed/k9s/issues/670)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.19.4.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.19.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## A Word From Out Sponsors...\n\nFirst off, I would like to send a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!\n\n* [Jason Vance](https://github.com/jasonvance)\n* [Jacob Gillespie](https://github.com/jacobwgillespie)\n\nMaintenance Release!\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #692](https://github.com/derailed/k9s/issues/692)\n* [Issue #689](https://github.com/derailed/k9s/issues/689)\n* [Issue #685](https://github.com/derailed/k9s/issues/685)\n* [Issue #684](https://github.com/derailed/k9s/issues/684)\n* [Issue #670](https://github.com/derailed/k9s/issues/670)\n* [PR #688](https://github.com/derailed/k9s/pull/688) All credits goes to [David Němec](https://github.com/davidnemec)!!\n* [PR #676](https://github.com/derailed/k9s/pull/676) Big Thanks to [Agrim Asthana](https://github.com/agrimrules)!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.19.5.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.19.5\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## A Word From Out Sponsors...\n\nFirst off, I would like to send a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!\n\n* [Tommy Dejbjerg Pedersen](https://github.com/tpedersen123)\n* [Matt Welke](https://github.com/mattwelke)\n\n## Disruption In The Force\n\nDuring this drop, I've gotten totally slammed by other forces ;( I've had so many disruptions that affected my `quasi` normal flow hence this drop might be a bit wonky ;( So please proceed with caution!!\n\nAs always please help me flush/report issues and I'll address them promptly! Thank you so much for your understanding and patience!! 🙏👨‍❤️‍👨😍\n\n## Improved Node Shell Usability\n\nIn this drop we've changed the configuration of the node shell action that lets you shell into nodes. Big thanks to [Patrick Decat](https://github.com/pdecat) for helping us flesh out this beta feature! We've added configuration to not only customize the image but also the resources and namespace on how to run these K9s pods on your clusters. The new configuration is set at the cluster scope level.\n\nHere is an example of the new pod shell config options:\n\n```yaml\n# $HOME/.k9s/config.yml\nk9s:\n  clusters:\n    blee:\n      featureGates:\n        # You must enable the nodeShell feature gate to enable shelling into nodes\n        nodeShell: true\n      # NEW! You can now tune the pod specification: currently image, namespace and resources\n      shellPod:\n        image: cool_kid_admin:42\n        namespace: blee\n        limits:\n          cpu: 100m\n          memory: 100Mi\n```\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #714](https://github.com/derailed/k9s/issues/714)\n* [Issue #713](https://github.com/derailed/k9s/issues/713)\n* [Issue #708](https://github.com/derailed/k9s/issues/708)\n* [Issue #707](https://github.com/derailed/k9s/issues/707)\n* [Issue #705](https://github.com/derailed/k9s/issues/705)\n* [Issue #704](https://github.com/derailed/k9s/issues/704)\n* [Issue #702](https://github.com/derailed/k9s/issues/702)\n* [Issue #700](https://github.com/derailed/k9s/issues/700) Fingers and toes crossed ;)\n* [Issue #694](https://github.com/derailed/k9s/issues/694)\n* [Issue #663](https://github.com/derailed/k9s/issues/663) Partially - should be better launching in a given namespace ie k9s -n fred??\n* [Issue #702](https://github.com/derailed/k9s/issues/702)\n* [PR #709](https://github.com/derailed/k9s/pull/709) All credits goes to [Namco](https://github.com/namco1992)!!\n* [PR #706](https://github.com/derailed/k9s/pull/706) Big Thanks to [M. Tarık Yurt](https://github.com/mtyurt)!\n* [PR #704](https://github.com/derailed/k9s/pull/704) Atta Boy!! [psvo](https://github.com/psvo)\n* [PR #696](https://github.com/derailed/k9s/pull/696) Thank you! Credits to [Christian Köhn](https://github.com/ckoehn)\n* [PR #691](https://github.com/derailed/k9s/pull/691) Mega Thanks To [Pavel Tumik](https://github.com/sagor999)!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.19.6.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.19.6\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## A Word From Our Sponsors...\n\nFirst off, I would like to send a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!\n\n* [danysirota](https://github.com/danysirota)\n* [lampapetrol](https://github.com/lampapetrol)\n\nMaintenance Release!\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #719](https://github.com/derailed/k9s/issues/719)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.19.7.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.19.7\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\nMaintenance Release!\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #726](https://github.com/derailed/k9s/issues/726)\n* [Issue #724](https://github.com/derailed/k9s/issues/724)\n* [Issue #722](https://github.com/derailed/k9s/issues/722)\n* [Issue #721](https://github.com/derailed/k9s/issues/721)\n* [Issue #720](https://github.com/derailed/k9s/issues/720)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.20.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.20.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## ♫ The Sound Behind The Release ♭\n\nAnd now for something a `beat` different?\n\nI figured, why not share one of the tunes I was spinning when powering thru teh bugs? Might as well share the pain/pleasure right?\n\nI've just discovered this Turkish band, that I dig and figured I'll share it with you while you read these release notes...\n\n[Ruh - She Past Away](https://www.youtube.com/watch?v=B7f-opGKOyI)\n\nNOTE! Mind you I grew up on the `The Cure`, so likely not for everyone here 🙀\n\n## PortForward Revisited\n\nWhile performing port-forwards, it could be convenient to specify a given IP address vs 'localhost'\nfor the forwarding host. For this reason, we are introducing a configuration setting that allows you to set the host IP address for the port-forward dialog on a per cluster basis. The IP address currently defaults to `localhost`.\n\nBig Thanks and all credits goes to [Stowe4077](https://github.com/Stowe4077) (and that very cute dog!) for raising this issue in the first place!!\n\nIn order to change the configuration, edit your k9s config file as follows:\n\n```yaml\nk9s:\n  ...\n  clusters:\n    blee:\n      namespace:\n        active: \"\"\n        favorites:\n        - fred\n        - default\n      view:\n        active: po\n      portForwardAddress: 1.2.3.4\n```\n\n## And We've Got A Floater!\n\nI've been noodling on this feature for a while and thought it might be time to `float` this over to you guys... While operating on a cluster you may ask yourself: \"Hum... wonder which resources use configmap `fred`?\" Sure a quick grep through your manifests on disk will do fine, but what about the resources actually deployed on your cluster? Well my friends wonder no m'o, K9s knows!\nWhile navigating to your ConfigMap View a new option will appear `UsedBy` pressing `u` will reveal any resources that are currently referencing that ConfigMap. As of this drop, this feature will be available for the usual suspects namely: ConfigMaps, Secrets and ServiceAccounts. K9s scans managing resources and locate references from Env vars, Volumes or ServiceAccounts.\n\nNOTE: This feature is expensive to produce and might take a while to fully resolve on larger clusters! Also K9s referential scans might not be full proof and the paint is still fresh on this one so trade carefully! More resources refs checks will be enabled once we've rinse and repeat on this deal. We hope you'll find this feature useful, if so, please make some noise!\n\n## Lastly...\n\nThere has been quick a bit of surgery going on with this drop, so this release could be a bit unstable. Please watch out for that carp overbite! As always, Thank You All for your understanding, support and patience!!\n\n## Resolved Bugs/Features/PRs\n\n- [Issue #734](https://github.com/derailed/k9s/issues/734)\n- [Issue #733](https://github.com/derailed/k9s/issues/733)\n- [Issue #716](https://github.com/derailed/k9s/issues/716)\n- [Issue #693](https://github.com/derailed/k9s/issues/693)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.20.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.20.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## ♫ The Sound Behind The Release ♭\n\nAnd now for something a `beat` different?\n\nI figured, why not share one of the tunes I was spinning when powering thru teh bugs? Might as well share the pain/pleasure right?\n\nI've just discovered this Turkish band, that I dig and figured I'll share it with you while you read these release notes...\n\n[Ruh - She Past Away](https://www.youtube.com/watch?v=B7f-opGKOyI)\n\nNOTE! Mind you I grew up on the `The Cure`, so likely not for everyone here 🙀\n\n## PortForward Revisited\n\nWhile performing port-forwards, it could be convenient to specify a given IP address vs 'localhost'\nfor the forwarding host. For this reason, we are introducing a configuration setting that allows you to set the host IP address for the port-forward dialog on a per cluster basis. The IP address currently defaults to `localhost`.\n\nBig Thanks and all credits goes to [Stowe4077](https://github.com/Stowe4077) (and that very cute dog!) for raising this issue in the first place!!\n\nIn order to change the configuration, edit your k9s config file as follows:\n\n```yaml\nk9s:\n  ...\n  clusters:\n    blee:\n      namespace:\n        active: \"\"\n        favorites:\n        - fred\n        - default\n      view:\n        active: po\n      portForwardAddress: 1.2.3.4\n```\n\n## And We've Got A Floater!\n\nI've been noodling on this feature for a while and thought it might be time to `float` this over to you guys... While operating on a cluster you may ask yourself: \"Hum... wonder which resources use configmap `fred`?\" Sure a quick grep through your manifests on disk will do fine, but what about the resources actually deployed on your cluster? Well my friends wonder no m'o, K9s knows!\nWhile navigating to your ConfigMap View a new option will appear `UsedBy` pressing `u` will reveal any resources that are currently referencing that ConfigMap. As of this drop, this feature will be available for the usual suspects namely: ConfigMaps, Secrets and ServiceAccounts. K9s scans managing resources and locate references from Env vars, Volumes or ServiceAccounts.\n\nNOTE: This feature is expensive to produce and might take a while to fully resolve on larger clusters! Also K9s referential scans might not be full proof and the paint is still fresh on this one so trade carefully! More resources refs checks will be enabled once we've rinse and repeat on this deal. We hope you'll find this feature useful, if so, please make some noise!\n\n## Lastly...\n\nThere has been quick a bit of surgery going on with this drop, so this release could be a bit unstable. Please watch out for that carp overbite! As always, Thank You All for your understanding, support and patience!!\n\n## Resolved Bugs/Features/PRs\n\n- [Issue #734](https://github.com/derailed/k9s/issues/734)\n- [Issue #733](https://github.com/derailed/k9s/issues/733)\n- [Issue #716](https://github.com/derailed/k9s/issues/716)\n- [Issue #693](https://github.com/derailed/k9s/issues/693)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.20.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.20.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release!\n\nFixing a few issues in the v0.20 aftermath ;(\nThank you all for reporting these issues and for your patience!\n\n## Selection Marker\n\nIn this drop, we're adding the ability to set row mark ranges. There are situations where you've filtered a resource and need to delete part or all of the rows. In previous releases, you had to mark each rows one by one. Now you have the ability to select the beginning and end range and all rows in between will now be marked! To mark a single row, you can use `space`. To select rows between your initial mark to the current selection use `Ctrl-space`. To nuke all marked rows use `Ctrl-\\`. All credits and ATTA BOY! goes to [Ryan Richard](https://github.com/cfryanr) for suggesting this feature! Thank you Ryan!!\n\n## Logs Got Some TLC!\n\nPer [Raman Gupta](https://github.com/rocketraman) excellent suggestion, we've added a way to add a separator to your chatty logs to easily see the latest incoming logs. While in log view, you can now press `m` for mark to add the separator to the log stream. If you don't care about the log history and just want to see the latest incoming logs, pressing `c` will clear out the log viewer.\n\n## Resolved Bugs/Features/PRs\n\n- [Issue #741](https://github.com/derailed/k9s/issues/741)\n- [Issue #740](https://github.com/derailed/k9s/issues/740)\n- [Issue #739](https://github.com/derailed/k9s/issues/739)\n- [Issue #727](https://github.com/derailed/k9s/issues/727)\n- [Issue #723](https://github.com/derailed/k9s/issues/723)\n- [PR #725](https://github.com/derailed/k9s/pull/725) Big Thanks To [Soupyt](https://github.com/soupyt)!!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.20.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.20.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n**Maintenance Release!**\n\n## Resolved Bugs/Features/PRs\n\n- [Issue #752](https://github.com/derailed/k9s/issues/752)\n- [Issue #677](https://github.com/derailed/k9s/issues/677) Once again with feelings this time ;(\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.20.4.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.20.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## PersistentVolumeClaims Reference Tracking\n\nIn continuation with the resource usage check feature added in v0.20, we've added reference checks on the PVC view. If you ever wonder which resources on your cluster are referencing a given PVC, simply press `u` for `UsedBy` and k9s will tell you.\n\n## New Config On The Block\n\nSome folks voiced concerns with K9s config dir littering their home directory with yet another `.dir`. In this drop, we're introducing a new env variable `K9SCONFIG` that tells K9s where to look for its configurations. If `K9SCONFIG` is not set K9s will look in the usual place aka `$HOME/.k9s`.\n\n## Resolved Bugs/Features/PRs\n\n- [Issue #754](https://github.com/derailed/k9s/issues/754)\n- [Issue #753](https://github.com/derailed/k9s/issues/753)\n- [Issue #743](https://github.com/derailed/k9s/issues/743)\n- [Issue #728](https://github.com/derailed/k9s/issues/728)\n- [Issue #718](https://github.com/derailed/k9s/issues/718)\n- [Issue #643](https://github.com/derailed/k9s/issues/643)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.20.5.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.20.5\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nAlso if you dig this tool, consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## A Word From Our Sponsors...\n\nFirst off, I would like to send a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!\n\n* [João Costa](https://github.com/JD557)\n\nMaintenance Release!\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #756](https://github.com/derailed/k9s/issues/756)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.21.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.21.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## First A Word From Our Sponsors...\n\nFirst off, I would like to send a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!\n\n* [Remo Eichenberger](https://github.com/remoe)\n* [Ken Ahrens](https://github.com/kenahrens)\n\n## Moving Forward!\n\nIn this drop, we've added a port-forward indicator to visually see if a port-forward is active on a pod/container. You can also navigate directly to the port-forward view using the new shortcut `f` available in\npod and container view.\n\n## Manifest That!\n\nEver wanted to manipulate your Kubernetes manifests directly in K9s? `Yes Please!!`\n\nWe are introducing a new view namely `directory` aka `dir`. Using this command you can list/traverse a given directory structure containing your Kubernetes manifests using a new `:dir /fred` command.\nFrom there you can view/edit your manifests and also deploy or delete these resources for your cluster directly from K9s. Just like `kubectl` you can apply/delete an entire directory or a single manifest.\nHow cool is that?\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #778](https://github.com/derailed/k9s/issues/778)\n* [Issue #774](https://github.com/derailed/k9s/issues/774)\n* [Issue #761](https://github.com/derailed/k9s/issues/761)\n* [Issue #759](https://github.com/derailed/k9s/issues/759)\n* [Issue #758](https://github.com/derailed/k9s/issues/758)\n* [PR #746](https://github.com/derailed/k9s/pull/746) Big Thanks to [Groselt](https://github.com/groselt)!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.21.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.21.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## A Word From Our Sponsors...\n\nI would like to send a `Big Thank You` to the following generous K9s friend for joining our sponsorship program and supporting this project!\n\n* [Joao Azevedo](https://github.com/jcazevedo)\n\nMaintenance Release!\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #791](https://github.com/derailed/k9s/issues/791)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.21.10.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.21.10\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\nMaintenance Release!\n\n## A Word From Our Sponsors...\n\nFirst off, I would like to send a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!\n\n* [Martin Kemp](https://github.com/MartiUK)\n\nContrarily to popular belief, OSS is not free! We've now reached ~9k stars and 300k downloads! As you all know, this project is not pimped out by a big company with deep pockets and a large team. K9s is complex and does demand a lot of my time. So if this tool is useful to you and part of your daily lifecycle, please contribute! Your contribution whether financial, PRs, issues or shout-outs on social/blogs are crucial to keep K9s growing and powerful for all of us. Don't let OSS by individual contributors become an oxymoron!\n\n## I Should've known better\n\nSeems like I've broken the golden rule ie never add a feature without providing an option to turn it off ;( It looks like enable mouse support for K9s had unexpected side effects. So in this drop, we're introducing a new configuration aka `enableMouse` that defaults to `false`. You can opt-in mouse support, by enabling it in the K9s config file. That said when mouse support is enabled, you can still use terminal selection using either `Shift/Option` for Windows/Mac.\n\n```yaml\n# $HOME/.k9s/config.yml\nk9s:\n  refreshRate: 2\n  enableMouse: true # Defaults to false if not set\n  headless: false\n  ...\n```\n\n## Resolved Issues/Features\n\n* [Issue #874](https://github.com/derailed/k9s/issues/874) Latest version broke selecting text by mouse\n\n## Resolved PRs\n\n* [PR #877](https://github.com/derailed/k9s/pull/877) Change character used for X in RBAC view. Thank you! [Torjus](https://github.com/torjue)\n* [PR #876](https://github.com/derailed/k9s/pull/876) Migrate to new sortorder import path. Big thanks to [fbbommel](https://github.com/fvbommel)\n* [PR #873](https://github.com/derailed/k9s/pull/873) Fix default logger config, same as README. Thank you! [darklore](https://github.com/darklore)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.21.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.21.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## First A Word From Our Sponsors...\n\nFirst off, I would like to send a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!\n\n* [Remo Eichenberger](https://github.com/remoe)\n* [Ken Ahrens](https://github.com/kenahrens)\n\nMaintenance Release!\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #790](https://github.com/derailed/k9s/issues/790) My bad! Must get mo' sleep ;(\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.21.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.21.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## ♫ The Sound Behind The Release ♭\n\nAnd now for something a `beat` different?\n\nI figured, why not share one of the tunes I was spinning when powering thru teh bugs? Might as well share the pain/pleasure right?\n\n[Funkin' for Jamaica](https://www.youtube.com/watch?v=uuUy2ShGLyo) by the most awesome Tom Browne!\n\nMaintenance Release!\n\nLots of bugs fix in this drop and perf improvements...\n\nNOTE! You may experience some disturbance in the force in this drop, so please proceed with caution and do land a hand flushing out potential issues.\nThank you!!\n\n## Video Tutorial\n\n[Who Let The Pods Out (v0.21.3)](https://youtu.be/wG8KCwDAhnw)\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #816](https://github.com/derailed/k9s/issues/816)\n* [Issue #813](https://github.com/derailed/k9s/issues/813)\n* [Issue #812](https://github.com/derailed/k9s/issues/812)\n* [Issue #810](https://github.com/derailed/k9s/issues/810)\n* [Issue #807](https://github.com/derailed/k9s/issues/807)\n* [Issue #806](https://github.com/derailed/k9s/issues/806)\n* [Issue #805](https://github.com/derailed/k9s/issues/805)\n* [Issue #800](https://github.com/derailed/k9s/issues/800)\n* [Issue #799](https://github.com/derailed/k9s/issues/799)\n* [Issue #709](https://github.com/derailed/k9s/issues/709) Crossing fingers and toes ;)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.21.4.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.21.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better is as ever very much noticed and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\nMaintenance Release! The aftermath...\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #819](https://github.com/derailed/k9s/issues/819)\n* [Issue #818](https://github.com/derailed/k9s/issues/818)\n* [Issue #797](https://github.com/derailed/k9s/issues/797)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.21.5.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.21.5\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## First A Word From Our Sponsors...\n\nFirst off, I would like to send a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!\n\n* [Drew](https://github.com/ScubaDrew)\n* [Vladimir Rybas](https://github.com/vrybas)\n\nContrarily to popular belief, OSS is not free! We've now reached 8k stars and 270k downloads! As you all know, this project is not pimped out by a big company with deep pockets or a large team. This project is complex and does demand a lot of my time. So if k9s is useful to you and part of your daily lifecycle. Please contribute! Your contribution whether financial, PRs, issues or shout-outs on social/blogs are crucial to keep K9s growing and powerful for all of us!\nDon't let OSS by individual contributors become an oxymoron...\n\n## New Skins On The Block!\n\nIn this drop, big thanks are in effect for [Dan Mikita](https://github.com/danmikita) for contributing a new K9s [solarized theme](https://github.com/derailed/k9s/tree/master/skins)!\n\nAlso we've added a new skin configuration for table's cursor namely `cursorFgColor` and `cursorBgColor`:\n\n```yaml\n  # skin.yml\n  ...\n  views:\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      cursorFgColor: *foreground\n      cursorBgColor: *current_line\n      header:\n        fgColor: white\n        bgColor: *background\n        sorterColor: *cyan\n  ...\n```\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #826](https://github.com/derailed/k9s/issues/826)\n* [Issue #824](https://github.com/derailed/k9s/issues/824)\n* [Issue #823](https://github.com/derailed/k9s/issues/823)\n* [Issue #821](https://github.com/derailed/k9s/issues/821)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.21.6.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.21.6\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## New Skins On The Block. Part Duh!\n\nIn this drop, we've added a new skin configuration for table's cursor namely `cursorFgColor` and `cursorBgColor` as well as the ability to skin your dialogs:\n\n```yaml\n  # skin.yml\nk9s:\n  ...\n  # Note: You can now skin your dialogs.\n  dialog:\n    fgColor: *foreground\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *magenta\n    buttonFocusFgColor: white\n    buttonFocusBgColor: *cyan\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  ...\n  views:\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      # Note! new tags\n      cursorFgColor: *foreground\n      cursorBgColor: *current_line\n      header:\n        fgColor: white\n        bgColor: *background\n        sorterColor: *cyan\n  ...\n```\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #795](https://github.com/derailed/k9s/issues/795)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.21.7.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.21.7\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\nMaintenance Release!\n\n## Resolved Bugs/Features/PRs\n\n* [Issue #281](https://github.com/derailed/k9s/issues/281)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.21.8.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.21.8\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\nMaintenance Release!\n\n## ♫ The Sound Behind The Release ♭\n\nI figured, why not share one of the tunes I was spinning when powering thru teh bugs? Might as well share the pain/pleasure while viewing this release notes right?\n\n[Strange Ritual - David Byrne](https://www.youtube.com/watch?v=gsramZ3sOjI) ;)\n\n## A Word From Our Sponsors...\n\nFirst off, I would like to send a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!\n\n* [Jean-Luc Geering](https://github.com/jlgeering)\n* [Takafumi Ikeda](https://github.com/ikeike443)\n\nContrarily to popular belief, OSS is not free! We've now reached ~9k stars and 300k downloads! As you all know, this project is not pimped out by a big company with deep pockets and a large team. K9s is complex and does demand a lot of my time. So if this tool is useful to you and part of your daily lifecycle, please contribute! Your contribution whether financial, PRs, issues or shout-outs on social/blogs are crucial to keep K9s growing and powerful for all of us. Don't let OSS by individual contributors become an oxymoron!\n\n## Resolved Issues/Features\n\n* [Issue #871](https://github.com/derailed/k9s/issues/871) K9s memory leak when shell that launched k9s is terminated.\n* [Issue #857](https://github.com/derailed/k9s/issues/857) Working in readonly mode.\n* [Issue #855](https://github.com/derailed/k9s/issues/855) Some mouse support.\n* [Issue #849](https://github.com/derailed/k9s/issues/849) Xray highlight color.\n* [Issue #845](https://github.com/derailed/k9s/issues/845) CronJob trigger checks wrong permission.\n* [Issue #837](https://github.com/derailed/k9s/issues/837) Hang after running plugin.\n\n## Resolved PRs\n\n* [PR #866](https://github.com/derailed/k9s/pull/866) Go 1.15 support convert int to string failure. Thank you [Trung](https://github.com/runlevel5)!\n* [PR #864](https://github.com/derailed/k9s/pull/864) Add ppc64le target. Thank you once again [Trung](https://github.com/runlevel5)!\n* [PR #863](https://github.com/derailed/k9s/pull/863) Update images in Dockerfile. Big thanks to [Peter Sutter](https://github.com/petersutter)!\n* [PR #841](https://github.com/derailed/k9s/pull/841) Fix a type in bug report template. Thanks to [Jinsu Park](https://github.com/umi0410)!\n* [PR #834](https://github.com/derailed/k9s/pull/834) Add Chocolatey installation. Thanks to [Romain](https://github.com/romch007)!\n* [PR #828](https://github.com/derailed/k9s/pull/828) Add solarized dark skin. Big Thanks to [Dan Mikita](https://github.com/danmikita)!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.21.9.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.21.9\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\nMaintenance Release!\n\n## ♫ The Sound Behind The Release ♭\n\nI figured, why not share one of the tunes I was spinning when powering thru teh bugs? Might as well share the pain/pleasure while viewing this release notes right?\n\n[Strange Ritual - David Byrne](https://www.youtube.com/watch?v=gsramZ3sOjI) ;)\n\n## A Word From Our Sponsors...\n\nFirst off, I would like to send a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!\n\n* [Jean-Luc Geering](https://github.com/jlgeering)\n* [Takafumi Ikeda](https://github.com/ikeike443)\n\nContrarily to popular belief, OSS is not free! We've now reached ~9k stars and 300k downloads! As you all know, this project is not pimped out by a big company with deep pockets and a large team. K9s is complex and does demand a lot of my time. So if this tool is useful to you and part of your daily lifecycle, please contribute! Your contribution whether financial, PRs, issues or shout-outs on social/blogs are crucial to keep K9s growing and powerful for all of us. Don't let OSS by individual contributors become an oxymoron!\n\n## Resolved Issues/Features\n\n* [Issue #871](https://github.com/derailed/k9s/issues/871) K9s memory leak when shell that launched k9s is terminated (With feeling!)\n* [Issue #849](https://github.com/derailed/k9s/issues/849) Xray highlight color (with feeling!)\n\n## Resolved PRs\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.22.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.22.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\nMaintenance Release!\n\n## A Word From Our Sponsors...\n\nFirst off, I would like to send a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!\n\n* [Martin Kemp](https://github.com/MartiUK)\n\nContrarily to popular belief, OSS is not free! We've now reached ~9k stars and 300k downloads! As you all know, this project is not pimped out by a big company with deep pockets and a large team. K9s is complex and does demand a lot of my time. So if this tool is useful to you and part of your daily lifecycle, please contribute! Your contribution whether financial, PRs, issues or shout-outs on social/blogs are crucial to keep K9s growing and powerful for all of us. Don't let OSS by individual contributors become an oxymoron!\n\n## I Should've known better\n\nSeems like I've broken the golden rule ie never add a feature without providing an option to turn it off ;( It looks like enable mouse support for K9s had unexpected side effects. So in this drop, we're introducing a new configuration aka `enableMouse` that defaults to `false`. You can opt-in mouse support, by enabling it in the K9s config file. That said when mouse support is enabled, you can still use terminal selection using either `Shift/Option` for Windows/Mac.\n\n```yaml\n# $HOME/.k9s/config.yml\nk9s:\n  refreshRate: 2\n  enableMouse: true # Defaults to false if not set\n  headless: false\n  ...\n```\n\n## Resolved Issues/Features\n\n* [Issue #874](https://github.com/derailed/k9s/issues/874) Latest version broke selecting text by mouse\n\n## Resolved PRs\n\n* [PR #877](https://github.com/derailed/k9s/pull/877) Change character used for X in RBAC view. Thank you! [Torjus](https://github.com/torjue)\n* [PR #876](https://github.com/derailed/k9s/pull/876) Migrate to new sortorder import path. Big thanks to [fbbommel](https://github.com/fvbommel)\n* [PR #873](https://github.com/derailed/k9s/pull/873) Fix default logger config, same as README. Thank you! [darklore](https://github.com/darklore)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.22.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.22.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\nMaintenance Release!\n\n## Resolved Issues/Features\n\n* [Issue #882](https://github.com/derailed/k9s/issues/882) After filtering objects cannot enter them anymore\n* [Issue #881](https://github.com/derailed/k9s/issues/881) CPU limit percentage in pod view counts containers without limits\n* [Issue #880](https://github.com/derailed/k9s/issues/880) filtering/search doesn't take all columns into account anymore\n\n## Resolved PRs\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.23.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.23.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\nI figured why not share one of the tunes I was spinning when powering thru teh bugs? Might as well share the pain/pleasure while viewing these release notes!\n\n* [On An Island - David Gilmour With Crosby&Nash](https://www.youtube.com/watch?v=kEa__0wtIRo)\n* [Cause We've Ended As Lovers - Jeff Beck](https://www.youtube.com/watch?v=VC02wGj5gPw)\n\n## Our Release Heroes\n\nPlease join me in recognizing and applauding this drop contributors that went the extra mile to make sure K9s is better and more useful for all of us!!\n\nBig ATTA BOY/GIRL! in full effect this week to the good folks below for their efforts and contributions to K9s!!\n\n* [Michael Albers](https://github.com/michaeljohnalbers)\n* [Wi1dcard](https://github.com/wi1dcard)\n* [Saskia Keil](https://github.com/SaskiaKeil)\n* [Tomasz Lipinski](https://github.com/tlipinski)\n* [Antoine Méausoone](https://github.com/Ameausoone)\n* [Emeric Martineau](https://github.com/emeric-martineau)\n* [Eldad Assis](https://github.com/eldada)\n* [David Arnold](https://github.com/blaggacao)\n* [Peter Parente](https://github.com/parente)\n\n## A Word From Our Sponsors...\n\nFirst off I would like to send a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!\n\n* [William Alexander](https://github.com/carpetfuz)\n* [Jiri Valnoha](https://github.com/waldauf)\n* [Pavel Tumik](https://github.com/sagor999)\n* [Bart Plasmeijer](https://github.com/bplasmeijer)\n* [Matt Welke](https://github.com/mattwelke)\n* [Stefan Mikolajczyk](https://github.com/stefanmiko)\n\nContrarily to popular belief, OSS is not free! We've now reached ~9k stars and 300k downloads! As you all know, this project is not pimped out by a big company with deep pockets and a big dev team. K9s is complex and does demand lots of my time. So if this tool is useful to you and your organization and part of your daily Kubernetes flow, please contribute! Your contribution whether financial, PRs, issues or shout-outs on social/blogs are crucial to keep K9s growing and powerful for all of us. Don't let OSS by individual contributors become an oxymoron!\n\n## Describe/YAML goes FullMonty!!\n\nWe've added a new option to enable full screen while describing or viewing a resource YAML. Similarly to the full screen toggle option in the log view, pressing `f` will now toggle full-screen for both YAML and Describe views.\n\nAdditionally, the YAML and Describe view are now reactive! YAML/Describe views will now watch for changes to the underlying resource manifests. I'll admit this was a feature I was missing, but decided to punt as it required a bit of re-org to make it happen correctly. So BIG thanks to [Fabian-K](https://github.com/Fabian-K) for entering this issue and for the boost!!\n\nNot cool enough for Ya? the YAML view now also affords for getting ride of those pesky `managedFields` while viewing a resource. Use the `m` key to toggle visibility on the managedFields.\n\n## Best Effort... Not!\n\nIn this drop, we've added 2 new columns namely `CPU/R:L` and `MEM/R:L`. These represents the current request:limit specified on containers. They are available in node, pod and container views. While in Pod view, you will need to volunteer them and use the `Go Wide` option `Ctrl-W` to see the columns. These columns will be display by default for Node/Container views. In the node view, they tally the total amount of resources for all pods hosted a given node. If that's inadequate, you can also leverage K9s [Custom Column](https://github.com/derailed/k9s#resource-custom-columns) feature to volunteer them or not.\n\n## Set Container Images\n\nYou will have the ability to tweak your container images for experimentation, using the new SetImage binding aka `i`. This feature is available for un-managed pods, deployments, statefulsets and daemonsets. With a resource selected, pressing `i` will provision an edit dialog listing all init/container images. So you will have to ability to tweak the images and update your containers. Big Thanks to [Antoine Méausoone](https://github.com/Ameausoone) for making this feature available to all of us!!\n\nNOTE! This is a one shot commands applied directly against your cluster and won't survive a new resource deployment.\n\n## Crumbs On...Crumbs Off, Caterpillar\n\nWe've added a new configuration to turn off the crumbs via `crumbsLess` configuration option. You can also toggle the crumbs via the new key option `Ctrl-g`. You can enable/disable this option in your ~/.k9s/config.yml or via command line using `--crumbsless` CLI option.\n\n```yaml\nk9s:\n  refreshRate: 2\n  headless: false\n  crumbsless: false\n  readOnly: true\n  ...\n```\n\n## BANG FILTERS!\n\nSome folks have voiced the desire to use inverse filters to refine content while in resource table views. Appending a `!` to your filter will now enable an inverse filtering operation For example, in order to see all pods that do not contain `fred` in their name, you can now use `/!fred` as your filtering command. If you dig this implementation, please make sure to give a big thank you to [Michael Albers](https://github.com/michaeljohnalbers) for the swift implementation!\n\n## New Conf On the Block...\n\nIn this release, we've made some changes to the retry policies when things fail on your cluster and the api-server is suffering from an hearing impediment. The current policy was to check for connection issues every 15secs and retry 15 times before exiting K9s. This rules were not configurable and could yield for overtaxing the api-server. So we've implemented exponential back-off so that K9s can attempt to remediate or bail out of the session if not.\nTo this end, there is a new config option namely `maxConnRetry` to will be added to your K9s config to set the retry policy. The default is currently set to 5 retries.\n\nNOTE: This is likely an ongoing story and more will come based on your feedback!\n\nSample K9s configuration\n\n```yaml\nk9s:\n  refreshRate: 2\n  # Set the maximum attempt to reconnect with the api-server in case of failures.\n  maxConnRetry: 5\n  ...\n```\n\n## 🏁 Start Your Engines...\n\nAs you can see, this is a pretty big drop and likely we've created some new issues in the process 🙀\n\nPlease make sure to file issues/PRs if things are not working as expected so we can improve on these features.\n\n👻 Happy Halloween To All!! (as if 2020 is not scary enough 🙈)\n\nThank you all for your being fans and supporting K9s!!\n\n---\n\n## Resolved Issues/Features\n\n* [Issue #906](https://github.com/derailed/k9s/issues/906) Print resources in pod view\n* [Issue #903](https://github.com/derailed/k9s/issues/903) Slow down reconnection rate on auth failures\n* [Issue #901](https://github.com/derailed/k9s/issues/901) Logs page for any pod/container shows Waiting for logs...\n* [Issue #900](https://github.com/derailed/k9s/issues/900) Support sort by pending status\n* [Issue #895](https://github.com/derailed/k9s/issues/895) Wrong highlight position when filtering logs\n* [Issue #892](https://github.com/derailed/k9s/issues/892) tacit kustomize & kpt support\n* [Issue #889](https://github.com/derailed/k9s/issues/889) Disable read only config via command line flag\n* [Issue #887](https://github.com/derailed/k9s/issues/887) Ability to call out a separate program to parse/filter logs\n* [Issue #886](https://github.com/derailed/k9s/issues/886) Full screen mode or remove borders in YAML view for easy copy/paste\n* [Issue #884](https://github.com/derailed/k9s/issues/884) Refresh for describe & yaml view\n* [Issue #883](https://github.com/derailed/k9s/issues/883) View logs quickly scrolls through entire logs when initially loading\n* [Issue #875](https://github.com/derailed/k9s/issues/875) Lazy filter\n* [Issue #848](https://github.com/derailed/k9s/issues/848) Support an inverse operator on filtered search\n* [Issue #820](https://github.com/derailed/k9s/issues/820) Log file spammed despite K9s not running\n* [Issue #794](https://github.com/derailed/k9s/issues/794) Events view\n\n## Resolved PRs\n\n* [PR #909](https://github.com/derailed/k9s/pull/909) Add support for inverse filtering\n* [PR #908](https://github.com/derailed/k9s/pull/908) Remove trailing delta from the scale dialog when replicas are in flux\n* [PR #907](https://github.com/derailed/k9s/pull/907) Improve docs on sinceSeconds logger option\n* [PR #904](https://github.com/derailed/k9s/pull/904) PVC `UsedBy` list irrelevant statefulsets\n* [PR #898](https://github.com/derailed/k9s/pull/898) Use config.CallTimeout in APIClient\n* [PR #897](https://github.com/derailed/k9s/pull/897) Use DefaultColorer for aliases rendering\n* [PR #896](https://github.com/derailed/k9s/pull/896) Allow remove crumbs\n* [PR #894](https://github.com/derailed/k9s/pull/894) Execute plugins and pass context\n* [PR #891](https://github.com/derailed/k9s/pull/891) Add command to get the latest stable kubectl version and support for KUBECTL_VERSION as Dockerfile ARG\n* [PR #847](https://github.com/derailed/k9s/pull/847) Add ability to set container images\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.23.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.23.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues/Features\n\n* [Issue #918](https://github.com/derailed/k9s/issues/918) NPE setting image. Totally on me ;(\n\n## Resolved PRs\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.23.10.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.23.10\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues/Features\n\n* [Issue #933](https://github.com/derailed/k9s/issues/933) Unable to cordon node starting in v0.23.8\n* [Issue #932](https://github.com/derailed/k9s/issues/932) Won't start if api.github.com is inaccessible\n* [Issue #931](https://github.com/derailed/k9s/issues/931) Describe ingress not showing labels\n\n## Resolved PRs\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.23.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.23.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n### Write Mode\n\nK9s is writable by default, meaning you can interact with your cluster and make changes using one shot commands ie edit, delete, scale, etc... There `readOnly` config option that can be specified in the configuration or via a cli arg to override this behavior. In this drop, we're introducing a symmetrical command line arg aka `--write` that overrides a K9s session and make it writable tho the readOnly config option is set to true.\n\n## Inverse Log Filtering\n\nIn the last drop, we've introduces reverse filters to filter out resources from table views. Now you will be able to apply inverse filtering on your log views as well via `/!fred`\n\n---\n\n## Resolved Issues/Features\n\n* [Issue #906](https://github.com/derailed/k9s/issues/906) Print resources in pod view. With Feelings. Thanks Claudio!\n* [Issue #889](https://github.com/derailed/k9s/issues/889) Disable readOnly config\n* [Issue #564](https://github.com/derailed/k9s/issues/564) Invert filter mode on logs\n\n## Resolved PRs\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.23.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.23.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release!\n\nArg.. Must get m'o sleep!!\n\n---\n\n## Resolved Issues/Features\n\n* [Issue #889](https://github.com/derailed/k9s/issues/889) Disable readOnly config\n\n## Resolved PRs\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.23.4.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.23.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues/Features\n\n* [Issue #920](https://github.com/derailed/k9s/issues/920) Timestamp stopped working\n* [Issue #663](https://github.com/derailed/k9s/issues/663) Perf issues in v0.23.X - Better??\n\n## Resolved PRs\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.23.5.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.23.5\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues/Features\n\n* [Issue #928](https://github.com/derailed/k9s/issues/928) Auto complete is too aggressive\n* [Issue #663](https://github.com/derailed/k9s/issues/663) Perf issues in v0.23.X - With feelings??\n\n## Resolved PRs\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.23.6.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.23.6\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release!\n\nBoyee! Having an awesome week here at the ranch!\nIt feels like k9s v0.23.X is plagued with as many election wining scenarios as the CNN's magic screen ;)\nTime to lay off the pipe... but before I do, here is another drop!\n\n### Use The Farce Luke!\n\nI've figured it might be a good time to come up with some notification when a new release is available. To this end, when a new k9w version has been released, you should see an indicator next to the k9s top screen `K9s Rev` section indicating an updated version is ready for mass consumption.\n\n### Thank you!\n\nI'd like to extend a big thank you to all that have reported issues with the drops and for being patient! I get the rapid k9s rev might be an issue for some, but I do try my best to make sure pri-1 issues are resolved quickly in order to make k9s better for all of us.\n\nThank you all for your understanding, kindness and support!!\n\n---\n\n## Resolved Issues/Features\n\n* [Issue #929](https://github.com/derailed/k9s/issues/929) Crash on startup with no metrics-server detected\n* [Issue #926](https://github.com/derailed/k9s/issues/926) JSON Do you have plans to apply\n\n## Resolved PRs\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.23.7.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.23.7\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues/Features\n\n* [Issue #930](https://github.com/derailed/k9s/issues/930) Version checker is not reporting a new release correctly\n\n## Resolved PRs\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.23.8.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.23.8\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues/Features\n\n* [Issue #663](https://github.com/derailed/k9s/issues/663) K9s is slow on large clusters (With feelings and crossing both fingers and toes)\n\n## Resolved PRs\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.23.9.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.23.9\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues/Features\n\n* [Issue #930](https://github.com/derailed/k9s/issues/930) Version checker is not reporting a new release correctly (With feelings...)\n\n## Resolved PRs\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.24.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.24.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\n* [Mother Protect - Niki & The Dove](https://www.youtube.com/watch?v=P5W2hjwBsFk)\n* [Dark Star - POLIÇA](https://www.youtube.com/watch?v=2pD3hJc-8xg)\n\n## A Word From Our Sponsors...\n\nFirst and foremost, I would like to extend a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!\nYour sponsorships efforts are vital to keep this project alive and evolving. So please do give back!\n\n* [Lopeg](https://github.com/lopeg)\n* [Gerhard Lazu](https://github.com/gerhard)\n\n---\n\n## Resolved Issues/Features\n\n* [Issue #953](https://github.com/derailed/k9s/issues/953) Pdb with percentages show as \"0\".\n* [Issue #947](https://github.com/derailed/k9s/issues/947) Selection is applied for nonexistent items.\n* [Issue #944](https://github.com/derailed/k9s/issues/944) Can not launch ksniff.\n* [Issue #940](https://github.com/derailed/k9s/issues/940) Indeterminate search results when filtering with numbers.\n* [Issue #914](https://github.com/derailed/k9s/issues/914) Unable to edit resources with colliding singular names.\n\n## Resolved PRs\n\n* [PR #941](https://github.com/derailed/k9s/pull/941) Add Monokai skin. My new favorite skin! Big Thanks to [Mike SigsWorth](https://github.com/mikesigs)!!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.24.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.24.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n☡ IMPORTANT!! v0.24.0 is a bad drop!! Apparently while upgrading the dependencies in the v0.24.0,  I've managed to hose the dialog's buttons focus hence producing the incorrect default button behavior. So please upgrade to v0.24.1 ASAP!!\n\n---\n\n## Resolved Issues/Features\n\n* [Issue #821](https://github.com/derailed/k9s/issues/821) Default color is no longer transparent.\n* [Issue #933](https://github.com/derailed/k9s/issues/933) Unable to cordon node.\n\n## Resolved PRs\n\n* [PR #941](https://github.com/derailed/k9s/pull/941) Add Monokai skin. My new favorite skin! Big Thanks to [Mike SigsWorth](https://github.com/mikesigs)!!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.24.10.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.24.10\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues\n\n* [Issue #1123](https://github.com/derailed/k9s/issues/1123) Cannot respond to keyboard strike after exit pod shell in windows 10\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.24.11.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.24.11\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n> NOTE: Made a mistake with the last release binaries including a release tag. My bad as his caused a headache for the good folks managing the release upstream. Reverted the change on this drop!\n\n---\n\n## Resolved Issues\n\n* [Issue #1163](https://github.com/derailed/k9s/issues/1163) Color for autocomplete text\n* [Issue #1153](https://github.com/derailed/k9s/issues/1153) Crash when scaling a deployment with a custom view\n* [Issue #1151](https://github.com/derailed/k9s/issues/1151) k9s does not use current namespace of current context\n* [Issue #1140](https://github.com/derailed/k9s/issues/1140) Can no longer trigger cronjobs manually\n* [Issue #1137](https://github.com/derailed/k9s/issues/1137) Unreadable container name\n* [Issue #1132](https://github.com/derailed/k9s/issues/1132) Searching for regex not always working\n* [Issue #1131](https://github.com/derailed/k9s/issues/1131) Changed release filenames starting k9s v0.24.10\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.24.12.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.24.12\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Prompt GOT Styles!\n\nAdded a new configuration for styling your command/search prompts. So you can now specify foreground/background and suggestion color to your heart content. For example:\n\n```yaml\n# $HOME/.k9s/skin.yml\nk9s:\n  body:\n    fgColor: aqua\n    bgColor: black\n    logoColor: purple\n  # Prompt styles\n  prompt:\n    fgColor: blue\n    bgColor: black\n    suggestColor: orange\n  ...\n```\n\n---\n\n## Resolved Issues\n\n* [Issue #1169](https://github.com/derailed/k9s/issues/1169) Scaling last deployment errors out\n* [Issue #1167](https://github.com/derailed/k9s/issues/1167) Cronjob trigger busted\n* [Issue #1163](https://github.com/derailed/k9s/issues/1163) Color for autocomplete text\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.24.13.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.24.13\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n### A Word From Our Sponsors...\n\nI want to recognize the following folks that have been kind enough to join our sponsorship program and pay it forward!\n\n* [Stephan Skydan](https://github.com/sskydan)\n* [Azar](https://github.com/azarudeena)\n* [Tim Orling](https://github.com/moto-timo)\n\nSo if you feel K9s is helping with your productivity while administering your Kubernetes clusters, please consider pitching in as it will go a long way in ensuring a thriving environment for this repo and our K9sers community at large.\n\nThank you!!\n\n---\n\n## Resolved Issues\n\n* [Issue #1182](https://github.com/derailed/k9s/issues/1169) Cronjob suspend does not work 0.24.12\n* [Issue #1167](https://github.com/derailed/k9s/issues/1167) Cronjob trigger busted with Feelings!\n\n## Resolved PRs\n\n* [PR #1141](https://github.com/derailed/k9s/pull/1141) Big Thanks to [Raul Cabello Martin](https://github.com/Raullllll) in making K9s better of all of us!!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.24.14.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.24.14\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues\n\n* [Issue #1186](https://github.com/derailed/k9s/issues/1186) Viewing previous logs does not work\n* [Issue #1167](https://github.com/derailed/k9s/issues/1167) Cronjob trigger busted with feelings!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.24.15.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.24.15\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\n* [Paradise Delay - Marteria, DJ Kose](https://www.youtube.com/watch?v=eM-xTN8ggOs)\n* [Fool For Your Stockings - ZZ Top - Sadly this one is a tribute to Dusty Hill ;(](https://www.youtube.com/watch?v=UExKTZ3veB8)\n\n---\n\n### A Word From Our Sponsors...\n\nI want to recognize the following folks that have been kind enough to join our sponsorship program and opted to `pay it forward`!\n\n* [Viacheslav Moskin](https://github.com/viacheslavmoskin)\n* [Thomas Peter Bernsten](https://github.com/tpberntsen)\n* [EMR-Bear](https://github.com/emrbear)\n\nSo if you feel K9s is helping with your productivity while administering your Kubernetes clusters, please consider pitching in as it will go a long way in ensuring a thriving environment for this repo and our K9sers community at large.\n\nThank you!!\n\n---\n\n## !!BREAKING CHANGE!!... We've moved!\n\nAs of this drop, k9s home directory is now configurable via [XDG](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html). Please see the specification depending on your platform of choice. You will now need to set or use the default for `$XDG_CONFIG_HOME` if not already present on your system. This is now the de facto replacement for`HOME/.k9s` as K9s will no longer honor this directory to load artifacts such as config, skins, views, etc... If you have existing customizations, you will need to move those over to your `$XDG_CONFIG_HOME/k9s` dir.\n\nThis feature is still fresh and we could have totally missed a piece, so please proceed with caution and keep that issue tracker handy...\n\nPlease join me in giving a Big Thank you! to [Arthur](https://github.com/pysen) for making this happen for us!\n\n---\n\n## Resolved Issues\n\n* [Issue #1209](https://github.com/derailed/k9s/issues/1209) K9s - Popeye run instructions\n* [Issue #1203](https://github.com/derailed/k9s/issues/1203) K9s does not remember last view I was in when switching contexts\n* [Issue #1181](https://github.com/derailed/k9s/issues/1181) Cannot list roles\n\n---\n\n## PRs\n\n* [PR #1213](https://github.com/derailed/k9s/pull/1213) Big Thanks to [Takumasa Sakao](https://github.com/sachaos)!\n* [PR #1205](https://github.com/derailed/k9s/pull/1205) Great catch from [David Alger](https://github.com/davidalger)!\n* [PR #1198](https://github.com/derailed/k9s/pull/1198) Once again [Takumasa Sakao](https://github.com/sachaos) to the rescue!!\n* [PR #1196](https://github.com/derailed/k9s/pull/1196) ATTA Boy! [Daniel Lee Harple](https://github.com/dlh)\n* [PR #1025](https://github.com/derailed/k9s/pull/1025) Big Thanks to [Arthur](https://github.com/pysen)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.24.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.24.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## ♫ Sounds Behind The Release ♭\n\n* [ZZ Top - My Head's in Mississippi](https://www.youtube.com/watch?v=Gp2PosHepzg)\n\n## A Word From Our Sponsors...\n\nI would like to extend a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!\n\n* [Tim Orling](https://github.com/moto-timo)\n* [Jiri Valnoha](https://github.com/waldauf)\n* [Osx2000](https://github.com/osx2000)\n\n## Our Release Heroes\n\nMajor ATTA BOY/GIRL! in full effect this week to the good folks below for their efforts and contributions in making sure K9s is better for all of us!\n\n* [Ainslie Hsu](https://github.com/ainslie-hsu)\n* [Lucas Teligioridis](https://github.com/lucasteligioridis)\n* [Gergely Tankovics](https://github.com/gtankovics)\n* [Michal Kuratczyk](https://github.com/mkuratczyk)\n* [Simon Caron](https://github.com/simoncaron)\n\n## She Can't Take Much More Capt'n!!\n\n### Background\n\nThanks to all of you for supporting K9s and being avid fans. I am truly humbled and amazed by your continued kindness and support!! As we're nearing K9s second anniversary, the project has reached over 10k stars and 384k downloads! That said, while these numbers sound stunning, there is another number on this project that is not so and that is number of sponsors 😿.\nAs I understand it, there are a several organizations leveraging K9s productivity to better their bottom line, without much care for ours...\nAs you all know, K9s is a complex tool in a continually evolving space and we find ourselves spending a lot of our free time, thinking, experimenting and supporting K9s to continually improve the offering. As it stands, there is currently a very small fraction of you that actively sponsor this project either financially or by filing issues/PRs while the rest are benefiting from these efforts. This just does not sound like a fair deal and if we were in the music business it would be a total outrage!\n\n### There Are Some That Call Me... Alpha!\n\nTo this end, I'd like to introduce a new member of the K9s pack, the main dog, aka `k9sAlpha`. This is going to be a licensed version of K9s. The current plan is to offer a tiered license scheme starting at `$10/month` for a license. K9s𝞪 will provide fixes, enhancements, further integrations and a bunch of new features that have been sitting in the back burner...\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9salpha.png\" align=\"center\" width=\"300\" height=\"auto\"/>\n\n### So what does this entail?\n\n1. The current k9s branch will be in feature freeze\n1. K9s𝞪 users will need to purchase a license from our store\n1. Active sponsors get a K9s𝞪 license\n1. Documentation, binaries, issue trackers, will be provisioned under a new K9s𝞪 site\n\nGiven any license schemes are meant to be hacked/broken, we're not going to over complicate things with calling out to license servers and such to ensure the keys are legit.\nThe current plan is to email out your license keys and trusting our `Gentlemen Agreement` that you will not share or distribute your keys to other folks.\nIn the current economic climate, if you can't afford a K9s𝞪 license, we will provide you one on a case by case basis.\n\nThe process should be simple:\n\n1. Acquire a license\n1. Get a key via email\n1. Store your key somewhere on disk\n1. Download the K9s𝞪 binary\n1. Administer your Kubernetes clusters with K9s𝞪\n1. Rinse and repeat when your license expires\n\n### K9s𝞪 Needs You!\n\nTo this end, I'd like to enlist a few of you to help me validate license keys, K9s𝞪 store and site to ensure the flow well... flows!\nIf you are so inclined, please reach out for your `shoephones` and send me an email with why you want to participate. Folks with K9s chops in multi clusters env would be preferred.\nIt should not take too much of your time to ensure all is cool, but want to make sure I have at least another 5 pairs of eyes to help out with the K9s𝞪 drop.\nMy hope is to get an initial K9s𝞪 revision dropped before Santa comes around...\n\n### Pipe In!\n\nBy all means, this is a democracy and not a dictatorship! So... if you have better/other ideas or concerns please pipe in! Open an issue on the repo so we can track, discuss, opiniate and figure out the best course of action that will be fair to both K9s maintainers and users alike.\n\n---\n\n## Resolved Issues/Features\n\n* [Issue #972](https://github.com/derailed/k9s/issues/972) Default color is no longer transparent.\n* [Issue #933](https://github.com/derailed/k9s/issues/933) Unable to cordon node.\n\n## Resolved PRs\n\n* [PR #982](https://github.com/derailed/k9s/pull/982) Fix typo\n* [PR #976](https://github.com/derailed/k9s/pull/976) Add OneDark color theme\n* [PR #975](https://github.com/derailed/k9s/pull/982) Handling non json lines as raw with red color\n* [PR #968](https://github.com/dserailed/k9s/pull/968) Disable filtering on help screen ... and broke the build ;)\n* [PR #960](https://github.com/derailed/k9s/pull/960) Handle empty port list in PortForward view\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.24.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.24.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## A Word From Our Sponsors...\n\nI would like to extend a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!\n\n* [Levkov](https://github.com/levkov)\n* [Michael McCafferty](https://github.com/mikemcc)\n* [Stephan Skydan](https://github.com/sskydan)\n* [Terrac Skiens](https://github.com/bluefishforsale)\n* [Zafer Abo-Samra](https://github.com/Inbiten)\n* [Gabriel Martinez](https://github.com/GMartinez-Sisti)\n* [Pierre Lebrun](https://github.com/pierreyves-lebrun)\n* [Luc Suryo](https://github.com/my10c)\n* [Sean O'Brien](https://github.com/sob)\n\n## Maintenance Release!\n\no Update Kubernetes to v0.20.5\n\n## There are some that call me... Alpha!\n\nK9s is still and will remain an open source software. As such it is free and we will continue to maintain this repo!\n\nThat said in order to support our efforts, we've recently launched [K9sAlpha](https://k9salpha.io) which is a freemium version of K9s. K9sAlpha unlocks additional features and enhancement.\n\nIf you would like to support us, you can either join our github sponsors or purchase a K9sAlpha license. If you are an active member of our github sponsorship program, you are eligible for a free K9sAlpha license. Please reach out for your shoe-phone and contact us for your personalized license key.\n\n<img src=\"https://k9salpha.io/assets/k9salpha-blue.png\" align=\"center\" width=\"300\" height=\"auto\"/>\n\n---\n\n## Resolved Issues\n\n* [Issue #1038](https://github.com/derailed/k9s/issues/1038) Release Cronjob API\n* [Issue #1035](https://github.com/derailed/k9s/issues/1035) Update Ingress API Group\n* [Issue #1028](https://github.com/derailed/k9s/issues/1028) Go compile\n* [Issue #1024](https://github.com/derailed/k9s/issues/1024) Add Pod Readiness/Nominated cols\n* [Issue #1013](https://github.com/derailed/k9s/issues/1013) Panic string negative repeat count\n* [Issue #1005](https://github.com/derailed/k9s/issues/1005) No x86_64 binaries\n* [Issue #735](https://github.com/derailed/k9s/issues/735) Shell into windows containers\n\n## Resolved PRs\n\n* [PR #1022](https://github.com/derailed/k9s/pull/1022) Update release\n* [PR #1012](https://github.com/derailed/k9s/pull/1012) Fix typo for cluster based skins\n* [PR #1009](https://github.com/derailed/k9s/pull/1009) Add webi installer info\n* [PR #1004](https://github.com/derailed/k9s/pull/1004) Correction CronJob ApiVersion\n* [PR #1026](https://github.com/derailed/k9s/pull/1026) Add option to hide logo\n* [PR #997](https://github.com/derailed/k9s/pull/997) Shell into windows containers\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.24.4.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.24.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## ♫ Sounds Behind The Release ♭\n\n* [The Dream - Albert Collins/Robert Cray](https://www.youtube.com/watch?v=XLkjF4s2Ms0)\n\n## A Word From Our Sponsors!\n\nI would like to extend a `Big Thank You` to the following generous K9s friends for joining our sponsorship program and supporting this project!\nWithout your support this project will be another cadaver in GitHub's infamous `Dead Program Society`. Thank you!!\n\n* 😻 [Antoine Meaussone](https://github.com/Ameausoone)\n\n## Maintenance Release!\n\n## There are some that call me... Alpha!\n\nK9s is still and will remain an open source software. As such it is free and we will continue to maintain this repo!\n\nThat said in order to support our efforts, we've recently launched [K9sAlpha](https://k9salpha.io) which is a freemium version of K9s. K9sAlpha unlocks additional features and enhancement.\n\nIf you would like to support us, you can either join our github sponsors or purchase a K9sAlpha license. If you are an active member of our github sponsorship program, you are eligible for a free K9sAlpha license. Please reach out for your shoe-phone and contact us for your personalized license key.\n\n<img src=\"https://k9salpha.io/assets/k9salpha-blue.png\" align=\"center\" width=\"300\" height=\"auto\"/>\n\n---\n\n## Resolved Issues\n\n* [Issue #1056](https://github.com/derailed/k9s/issues/1056) K9s hangs on edits\n* [Issue #1024](https://github.com/derailed/k9s/issues/1024) Add Pod Readiness/Nominated cols. With feelings!\n\n## Resolved PRs\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.24.5.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.24.5\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n## There are some that call me... Alpha!\n\nK9s is still and will remain an open source software. As such it is free and we will continue to maintain this repo!\n\nThat said in order to support our efforts, we've recently launched [K9sAlpha](https://k9salpha.io) which is a freemium version of K9s. K9sAlpha unlocks additional features and enhancements.\n\nIf you would like to support us, you can either join our github sponsors or purchase a K9sAlpha license. If you are an active member of our github sponsorship program, you are eligible for a free K9sAlpha license. Please reach out for your shoe-phone and contact us for your personalized license key.\n\n<img src=\"https://k9salpha.io/assets/k9salpha-blue.png\" align=\"center\" width=\"300\" height=\"auto\"/>\n\n---\n\n## Resolved Issues\n\n* [Issue #1063](https://github.com/derailed/k9s/issues/1063) Weird colors on windows (Don't do windows so please help verify!)\n* [Issue #1061](https://github.com/derailed/k9s/issues/1061) Container shell Windows (Don't do windows so please help verify!)\n* [Issue #1059](https://github.com/derailed/k9s/issues/1059) Monokai skin broken\\\n* [Issue #177](https://github.com/derailed/k9s/issues/177) Shell first character lost\n\n## Resolved PRs\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.24.6.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.24.6\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n## Our Release Heroes\n\nMajor ATTA BOY/GIRL! in full effect this week to the good folks below for their efforts and contributions in making sure K9s is better for all of us!\n\n* 🙏 [Arash Outadi](https://github.com/arashout)\n\n## There are some that call me... Alpha!\n\nK9s is still and will remain an open source software. As such it is free and we will continue to maintain this repo!\n\nThat said in order to support our efforts, we've recently launched [K9sAlpha](https://k9salpha.io) which is a freemium version of K9s. K9sAlpha unlocks additional features and enhancements.\n\nIf you would like to support us, you can either join our github sponsors or purchase a K9sAlpha license. If you are an active member of our github sponsorship program, you are eligible for a free K9sAlpha license. Please reach out for your shoe-phone and contact us for your personalized license key.\n\n<img src=\"https://k9salpha.io/assets/k9salpha-blue.png\" align=\"center\" width=\"300\" height=\"auto\"/>\n\n---\n\n## Resolved Issues\n\n* [Issue #1063](https://github.com/derailed/k9s/issues/1063) Weird colors on windows (Don't do windows so please help verify!)\n* [Issue #1061](https://github.com/derailed/k9s/issues/1061) Container shell Windows (Don't do windows so please help verify!)\n\n## Resolved PRs\n\n* [PR #1062](https://github.com/derailed/k9s/pull/1062) Add auto-refresh toggle for yaml and describe views. Now defaults to no refresh!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.24.7.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.24.7\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n## Disturbance In The Farce.. Windows!\n\nSplendid! So I had to borrow my neighbors kids 20 pounds windows `gaming` laptop for this one ;( Recent K9s drops are looking less than optimal on windows due to dependencies changes.\nI was able to narrow it down to named colors are no longer being respected on Windows platforms. I'll keep digging on this but if you find yourself in the situation where K9s is looking less than optimal on Windows, for the short term please either use a custom skin with hex colors or change the stock skin to use hex color values vs named colors. Thank you!\n\n## There are some that call me... Alpha!\n\nK9s is still and will remain an open source software. As such it is free and we will continue to maintain this repo!\n\nThat said in order to support our efforts, we've recently launched [K9sAlpha](https://k9salpha.io) which is a freemium version of K9s. K9sAlpha unlocks additional features and enhancements.\n\nIf you would like to support us, you can either join our github sponsors or purchase a K9sAlpha license. If you are an active member of our github sponsorship program, you are eligible for a free K9sAlpha license. Please reach out for your shoe-phone and contact us for your personalized license key.\n\n<img src=\"https://k9salpha.io/assets/k9salpha-blue.png\" align=\"center\" width=\"300\" height=\"auto\"/>\n\n---\n\n## Resolved Issues\n\n* [Issue #1067](https://github.com/derailed/k9s/issues/1067) Increase HPA target column display\n* [Issue #1061](https://github.com/derailed/k9s/issues/1061) Container shell Windows (Don't do windows so please help verify!)\n* [Issue #1060](https://github.com/derailed/k9s/issues/1060) Exception when setting container image\n\n## Resolved PRs\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.24.8.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.24.8\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n### NodeShell args\n\nIn this drop, we've added additional configurations to the k9s node shell so you override the command and args on the node shell containers.\n\n```yaml\n    # $HOME/.k9s/config.yml\n    ...\n    minikube:\n      view:\n        active: pod\n      featureGates:\n        nodeShell: true\n      shellPod:\n        image: busybox:1.31\n        # New!\n        command: [\"/bin/sh\", \"-c\"]\n        # New!\n        args: [\"ls -al\"]\n        namespace: default\n        limits:\n          cpu: 100m\n          memory: 100Mi\n     ...\n```\n\n---\n\n## Resolved Issues\n\n* [Issue #1106](https://github.com/derailed/k9s/issues/1106) Remove padding while in full screen\n* [Issue #1104](https://github.com/derailed/k9s/issues/1104) Config args for shellPod\n* [Issue #1102](https://github.com/derailed/k9s/issues/1102) Explicitly announce no metrics are available\n* [Issue #1097](https://github.com/derailed/k9s/issues/1097) Delete resource dialog stopped working\n* [Issue #1093](https://github.com/derailed/k9s/issues/1094) Leading comma in command column\n* [Issue #1094](https://github.com/derailed/k9s/issues/1094) Screendumps empty on EKS\n* [Issue #1060](https://github.com/derailed/k9s/issues/1060) Exception when setting container image\n* [Issue #1081](https://github.com/derailed/k9s/issues/1081) Color issue on startup\n* [Issue #1078](https://github.com/derailed/k9s/issues/1078) Nord skin\n* [Issue #1075](https://github.com/derailed/k9s/issues/1075) Crash on mouse click out of main window\n* [Issue #1070](https://github.com/derailed/k9s/issues/1070) lose cursor on windows 10\n* [Issue #1068](https://github.com/derailed/k9s/issues/1068) Build error 0.24.7\n* [Issue #1063](https://github.com/derailed/k9s/issues/1063) Weird colour scheme on windows\n\n## Resolved PRs\n\n* [PR #1101](https://github.com/derailed/k9s/pull/1101) propagate insecure-skip-tls-verify\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.24.9.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.24.9\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues\n\n* [Issue #1111](https://github.com/derailed/k9s/issues/1111) -A switch doesn't work as advertised\n* [Issue #1109](https://github.com/derailed/k9s/issues/1109) 0.24.8 edit needs an extra keystroke to process. (Crossing fingers AND toes!!)\n* [Issue #1104](https://github.com/derailed/k9s/issues/1104) Configure args for shellPod\n\n## Resolved PRs\n\n* [PR #1103](https://github.com/derailed/k9s/pull/1103) Dynamically load style for help. Big Thanks To [Louis Garman](https://github.com/leg100)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.25.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\n* [High Fidelity - By Elvis Costello (Yup! he started is career as a computer operator. Can u tell??)](https://www.youtube.com/watch?v=DJS-2kacmpU)\n* [Walk With A Big Stick - Foster The People](https://www.youtube.com/watch?v=XMY1VMTyl8s)\n* [Beirut - Steps Ahead -- Love this band!! with the ever so talented and sadly late Michael Brecker ;(](https://www.youtube.com/watch?v=UExKTZ3veB8)\n\n---\n\n### A Word From Our Sponsors...\n\nI want to recognize the following folks that have been kind enough to join our sponsorship program and opted to `pay it forward`!\n\n* [Andrew Regan](https://github.com/poblish)\n* [Bruno Brito](https://github.com/brunohbrito)\n* [ScubaDrew](https://github.com/ScubaDrew)\n* [mike-code](https://github.com/mike-code)\n* [Andrew Aadland](https://github.com/DaemonDude23)\n* [Michael Albers](https://github.com/michaeljohnalbers)\n\nSo if you feel K9s is helping with your productivity while administering your Kubernetes clusters, please consider pitching in as it will go a long way in ensuring a thriving environment for this repo and our K9sers community at large.\n\nAlso please take some time and give a huge shoot out to all the good folks below that have spent time plowing thru the code to help improve K9s for all of us!\n\nThank you!!\n\n---\n\n## Personal Note...\n\nI had so many distractions this cycle so expect some `disturbance in the farce!` on this drop.\nTo boot rat holed quiet a bit on improving speed. So I might have drop some stuff on the floor in the process...\nPlease report back if that's the case and we will address shortly. Tx!!\n\n## Port It Forward??\n\nEver been in a situation where you need to constantly port-forward on a given pod with multiple containers or exposing multiple ports? If so it might be cumbersome to have to type in the full container:port specification to activate a forward. If you fall in this use cases, you can now specify which container and port you would rather port-forward to by default. In this drop, we introduce a new annotation that you can use to specify and container/port to forward to by default. If set, the port-forward dialog will know to default to your settings.\n\n> NOTE: you can either use a container port name or number in your annotation!\n\n```yaml\n# Pod fred\napiVersion: v1\nkind: Pod\nmetadata:\n  name: fred\n  annotations:\n    k9scli.io/auto-portforwards: zorg::5556        # => will default to container zorg port 5556 and local port 5566. No port-forward dialog will be shown.\n    # Or...\n    k9scli.io/portforward: bozo::6666:p1           # => launches the port-forward dialog selecting default port-forward on container bozo port named p1(8081)\n                                                   # mapping to local port 6666.\n    ...\nspec:\n  containers:\n  - name: zorg\n    ports:\n    - name: p1\n      containerPort: 5556\n    ...\n  - name: bozo\n    ports:\n    - name: p1\n      containerPort: 8081\n    - name: p2\n      containerPort: 5555\n    ...\n```\n\nThe annotation value must specify a container to forward to as well as a local port and container port. The container port may be specified as either a port number or port name. If the local port is omitted then the local port will default to the container port number. Here are a few examples:\n\n1. bozo::http      - creates a pf on container `bozo` with port name http. If http specifies port number 8080 then the local port will be 8080 as well.\n2. bozo::9090:http - creates a pf on container `bozo` mapping local port 9090->http(8080)\n3. bozo::9090:8080 - creates a pf on container `bozo` mapping local port 9090->8080\n\n---\n\n## Resolved Issues\n\n* [Issue #1299](https://github.com/derailed/k9s/issues/1299) After upgrade to 0.24.15 sorting shortcuts not working\n* [Issue #1298](https://github.com/derailed/k9s/issues/1298) Install K9s through go get reporting ambiguous import error\n* [Issue #1296](https://github.com/derailed/k9s/issues/1296) Crash when clicking between border of K9s and terminal pane\n* [Issue #1289](https://github.com/derailed/k9s/issues/1289) Homebrew calling bottle :unneeded is deprecated! There is no replacement\n* [Issue #1273](https://github.com/derailed/k9s/issues/1273) Not loading config from correct default location when XDG_CONFIG_HOME is unset\n* [Issue #1268](https://github.com/derailed/k9s/issues/1268) Age sorting wrong for years\n* [Issue #1258](https://github.com/derailed/k9s/issues/1258) Configurable or recent use based port-forward\n* [Issue #1257](https://github.com/derailed/k9s/issues/1257) Why is the latest chocolatey on 0.24.10\n* [Issue #1243](https://github.com/derailed/k9s/issues/1243) Port forward fails in kind on windows 10\n\n---\n\n## PRs\n\n* [PR #1300](https://github.com/derailed/k9s/pull/1300) move from io/ioutil to io/os packages\n* [PR #1287](https://github.com/derailed/k9s/pull/1287) Add missing styles to kiss\n* [PR #1286](https://github.com/derailed/k9s/pull/1286) Some small color modifications\n* [PR #1284](https://github.com/derailed/k9s/pull/1284) Fix a small typo which comes from cluster view info\n* [PR #1271](https://github.com/derailed/k9s/pull/1271) Removed cursor colors that are too light to read\n* [PR #1266](https://github.com/derailed/k9s/pull/1266) Skin to preserve your terminal session background color\n* [PR #1264](https://github.com/derailed/k9s/pull/1205) Adding note on popeye config\n* [PR #1261](https://github.com/derailed/k9s/pull/1261) Blurry logo\n* [PR #1250](https://github.com/derailed/k9s/pull/1250) Gruvbox dark skin\n* [PR #1249](https://github.com/derailed/k9s/pull/1249) Node shell pod tolerate all taints\n* [PR #1232](https://github.com/derailed/k9s/pull/1232) Add red skin for production env\n* [PR #1227](https://github.com/derailed/k9s/pull/1227) Add abbreviation ReadWriteOncePod PV access mode\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.25.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\nLooks like we've broken a few little thingies...\nMay need a few rapid fires to regain some sanity so please bare with us and thank you for your reports!!\n\n---\n\n## Resolved Issues\n\n* [Issue #1308](https://github.com/derailed/k9s/issues/1308) Command auto-complete suggestions disappear after screen refresh interval #1308\n* [Issue #1307](https://github.com/derailed/k9s/issues/1307) Displayed Cluster name is always read from current-context\n* [Issue #1296](https://github.com/derailed/k9s/issues/1244) Scoobie-Doo was not a cow - NOTE: Switch to dialog to keep live context!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.10.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.25.10\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n### A Word From Our Sponsors...\n\nI want to recognize the following folks that have been kind enough to join our sponsorship program and opted to `pay it forward`!\n\n* [Joshua Kapellen](https://github.com/joshuakapellen)\n* [Qdentity](https://github.com/qdentity)\n* [Maxim](https://github.com/bsod90)\n* [Sönke Schau](https://github.com/xgcssch)\n\nSo if you feel K9s is helping with your productivity while administering your Kubernetes clusters, please consider pitching in as it will go a long way in ensuring a thriving environment for this repo and our K9sers community at large.\n\nAlso please take some time and give a huge shoot out to all the good folks below that have spent time plowing thru the code to help improve K9s for all of us!\n\nThank you!!\n\n---\n\n## Maintenance Release!\n\nDoh! Sorry ;( with feelings...\n\n---\n\n## Resolved Issues\n\n* [Issue #1361](https://github.com/derailed/k9s/issues/1361) Pulses not displaying graphs\n* [Issue #1358](https://github.com/derailed/k9s/issues/1358) Namespace list is empty\n* [Issue #1357](https://github.com/derailed/k9s/issues/1357) Benchmarks doesn't work on windows\n* [Issue #1355](https://github.com/derailed/k9s/issues/1355) Trace log level does not exist\n* [Issue #1345](https://github.com/derailed/k9s/issues/1345) Access denied after context switch\n\n---\n\n## PRs\n\n* [PR #1363](https://github.com/derailed/k9s/pull/1363) Add rose-pine skin.\n  [Sergio Soria](https://github.com/sasoria)\n* [PR #1356](https://github.com/derailed/k9s/pull/1356) Add flux trace shortcut to flux plugin.\n  [Guillaume Berche](https://github.com/gberche-orange)\n* [PR #1321](https://github.com/derailed/k9s/pull/1321) Add customizable dump directory property.\n  [Vlasov Artem](https://github.com/VlasovArtem)\n\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.11.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.25.11\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n### A Word From Our Sponsors...\n\nI want to recognize the following folks that have been kind enough to join our sponsorship program and opted to `pay it forward`!\n\n* [Joshua Kapellen](https://github.com/joshuakapellen)\n* [Qdentity](https://github.com/qdentity)\n* [Maxim](https://github.com/bsod90)\n* [Sönke Schau](https://github.com/xgcssch)\n\nSo if you feel K9s is helping with your productivity while administering your Kubernetes clusters, please consider pitching in as it will go a long way in ensuring a thriving environment for this repo and our K9sers community at large.\n\nAlso please take some time and give a huge shoot out to all the good folks below that have spent time plowing thru the code to help improve K9s for all of us!\n\nThank you!!\n\n---\n\n## Maintenance Release!\n\nHoy! end of year suck... Feeling the burn ;( Apologize for the disruptions...\n\n---\n\n## Resolved Issues\n\n* [Issue #1374](https://github.com/derailed/k9s/issues/1374) --all-namespaces does not work v0.25.10\n* [Issue #1376](https://github.com/derailed/k9s/issues/1376) Events not sorted correctly by dates\n* [Issue #1373](https://github.com/derailed/k9s/issues/1373) change namespace not possible\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.12.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.25.12\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n### A Word From Our Sponsors...\n\nI want to recognize the following folks that have been kind enough to join our sponsorship program and opted to `pay it forward`!\n\n* [Joshua Kapellen](https://github.com/joshuakapellen)\n* [Qdentity](https://github.com/qdentity)\n* [Maxim](https://github.com/bsod90)\n* [Sönke Schau](https://github.com/xgcssch)\n\nSo if you feel K9s is helping with your productivity while administering your Kubernetes clusters, please consider pitching in as it will go a long way in ensuring a thriving environment for this repo and our K9sers community at large.\n\nAlso please take some time and give a huge shoot out to all the good folks below that have spent time plowing thru the code to help improve K9s for all of us!\n\nThank you!!\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\n* [Border Patrol - Eek A Mouse](https://www.youtube.com/watch?v=pQVNzolpoII)\n* [All Mine - Portishead](https://www.youtube.com/watch?v=cuclNJiE8NY)\n* [Come on up to the house - Tom Waits](https://www.youtube.com/watch?v=9XVGAatyeNk)\n\n## Maintenance Release!\n\nHoy! end of year is... sucking! Feeling the burn ;( Apologies for the disruptions!!\n\n`You're either a pigeon or... the statue!`\n\n---\n\n## Resolved Issues\n\n* [Issue #1378](https://github.com/derailed/k9s/issues/1378) Regression: Namespace filters are no longer applied on startup\n* [Issue #1376](https://github.com/derailed/k9s/issues/1376) Events not sorted correctly by dates\n* [Issue #1375](https://github.com/derailed/k9s/issues/1375) Unable to show port forwards\n* [Issue #1374](https://github.com/derailed/k9s/issues/1374) --all-namespaces does not work v0.25.10\n* [Issue #1373](https://github.com/derailed/k9s/issues/1373) change namespace not possible\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.13.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.25.13\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n### A Word From Our Sponsors...\n\nI want to recognize the following folks that have been kind enough to join our sponsorship program and opted to `pay it forward`!\n\n* [uderik](https://github.com/uderik)\n* [Daimler](https://github.com/Daimler) wOOt!! Mercedes Benz sponsorship! How cool is that?\n\nSo if you feel K9s is helping with your productivity while administering your Kubernetes clusters, please consider pitching in as it will go a long way in ensuring a thriving environment for this repo and our K9sers community at large.\n\nAlso please take some time and give a huge shoot out to all the good folks below that have spent time plowing thru the code to help improve K9s for all of us!\n\nThank you!!\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\n* [Gash Dem - Chuck Fenda](https://www.youtube.com/watch?v=Y4NSYW4wusI)\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues\n\n* [Issue #1382](https://github.com/derailed/k9s/issues/1382) Watcher failed for screendumps\n* [Issue #1381](https://github.com/derailed/k9s/issues/1381) --request-timeout affects logs streaming\n* [Issue #1380](https://github.com/derailed/k9s/issues/1380) :pulse returning error: expecting a TableRow but got *v1.Table\n* [Issue #1376](https://github.com/derailed/k9s/issues/1376) Events are not sorted correctly by dates - with feelings...\n* [Issue #1291](https://github.com/derailed/k9s/issues/1291) K9s do not show any error when is unable to get logs, just do not show anything.\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.14.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.25.14\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release!\n\nDoh! Hot fix on the way...\n\n---\n\n## Resolved Issues\n\n* [Issue #1384](https://github.com/derailed/k9s/issues/1384) Leaving Logs View Causes Crash: \"panic: send on closed channel\"\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.15.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.25.15\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release!\n\nAye! Hot fix on the way...\n\n---\n\n## Resolved Issues\n\n* [Issue #1384](https://github.com/derailed/k9s/issues/1384) Leaving Logs View Causes Crash: \"panic: send on closed channel\" - with feelings!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.16.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.25.16\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n### A Word From Our Sponsors...\n\nI want to recognize the following folks that have been kind enough to join our sponsorship program and opted to `pay it forward`!\n\n* [Sebastian Racs](https://github.com/sebracs)\n* [Timothy C. Arland](https://github.com/tcarland)\n* [Julie Ng](https://github.com/julie-ng)\n\nSo if you feel K9s is helping with your productivity while administering your Kubernetes clusters, please consider pitching in as it will go a long way in ensuring a thriving environment for this repo and our K9sers community at large.\n\nAlso please take some time and give a huge shoot out to all the good folks below that have spent time plowing thru the code to help improve K9s for all of us!\n\nThank you!!\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\n[Blue Christmas - Fats Domino](https://www.youtube.com/watch?v=7jeo09zAskc)\n[Mele Kalikimaka - Bing Crosby](https://www.youtube.com/watch?v=hEvGKUXW0iI)\n[Cause - Rodriguez -- Spreading The Holiday Cheer! 🤨](https://www.youtube.com/watch?v=oKFkc19T3Dk)\n\n---\n\n## 🎅🎄 !!Merry Christmas To All!! 🎄🎅\n\nI hope you will take this time of the year to relax, re-source and spend quality time with your loved ones. I know it's been a `tad rocky` of recent ;( as I've gotten seriously slammed with work in the last few months...\nThe fine folks here on this channel have been nothing but kind, patient and willing to help, this humbles me! I feel truly blessed to be affiliated with our great `k9sers` community!\nNext month, we'll celebrate our anniversary as we've started out in this venture back in Jan 2019 (Yikes!) so get crack'in and iron out those bow ties already!!\n\nBest wishes for great health, happiness and continued success for 2022 to you all!!\n\n-Fernand\n\n---\n\n## A Christmas Story...\n\nAs of this drop, we've added a new feature to override the sort column and order for a given Kubernetes resource. This feature piggy backs of custom column views and add a new attribute namely `sortColumn`. For example say you'd like to set the default sort for pods to age descending vs name/namespace, you can now do the following in your `views.yml` file in the k9s config directory:\n\nNOTE: This file is live thus you can nav to your fav resource, change the column config and view the resource columns and sort changes... Woot!!\n\n```yaml\nk9s:\n  views:\n    v1/endpoints:\n      columns:\n        - NAME\n        - NAMESPACE\n        - ENDPOINTS\n        - AGE\n    v1/pods:\n      sortColumn: AGE:desc  # => suffix [:asc|:desc] for ascending or descending order.\n    v1/services:\n      ...\n```\n\n---\n\n## Resolved Issues\n\n* [Issue #1398](https://github.com/derailed/k9s/issues/1398) Pod logs containing brackets not in k9s logs output\n* [Issue #1397](https://github.com/derailed/k9s/issues/1397) Regression: k9s no longer starts in current context namespace since v0.25.12\n* [Issue #1358](https://github.com/derailed/k9s/issues/1358) Namespaces list is empty\n* [Issue #956](https://github.com/derailed/k9s/issues/956) Feature request : Default column sort (by resource view)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.17.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.25.17\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release\n\n---\n\n## Resolved Issues\n\n* [Issue #1402](https://github.com/derailed/k9s/issues/1402) Sort functionality does not work properly on v0.25.16\n* [Issue #1401](https://github.com/derailed/k9s/issues/1401) Nothing selected when last item deleted\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.18.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.25.18\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release\n\n---\n\n## Resolved Issues\n\n* [Issue #1402](https://github.com/derailed/k9s/issues/1402) Sort functionality does not work properly on v0.25.16. With Feelings!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.19.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.25.19\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated! Also big thanks to all that have allocated their own time to help others on both slack and this repo!!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release\n\n---\n\n## Resolved Issues\n\n* [Issue #1609](https://github.com/derailed/k9s/issues/1609) K9s fails to launch when active view does not exist\n* [Issue #1593](https://github.com/derailed/k9s/issues/1593) Selection of namespace is changed automatically\n* [Issue #1572](https://github.com/derailed/k9s/issues/1572) Wrong resource configuration being display after updating ingress\n* [Issue #1569](https://github.com/derailed/k9s/issues/1569) Slight wording error when port forward already existS!\n* [Issue #1565](https://github.com/derailed/k9s/issues/1565) Popeye stopped working\n\n## Resolved PR\n\n* [PR #1601](https://github.com/derailed/k9s/pull/1601) Ensure correct text in prompt when suspending cronjob\n* [PR #1600](https://github.com/derailed/k9s/pull/1600) Fix typo in fastforwards annotation name\n* [PR #1566](https://github.com/derailed/k9s/pull/1566) Correct typo in skins\n* [PR #1555](https://github.com/derailed/k9s/pull/1555) Update benchmark command in readme\n* [PR #1553](https://github.com/derailed/k9s/pull/1553) Allow `all` deletion propagation policy\n* [PR #1539](https://github.com/derailed/k9s/pull/1539) Plugin to allow default chart values retrieval\n* [PR #1529](https://github.com/derailed/k9s/pull/1529) Update example k9s config file\n* [PR #1518](https://github.com/derailed/k9s/pull/1518) Add Helm values support\n* [PR #1493](https://github.com/derailed/k9s/pull/1493) Fix padding is not 0 in fullscreen\n* [PR #1422](https://github.com/derailed/k9s/pull/1422) Fix typo in README\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.25.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\nLooks like we've broken a few little thingies...\nMay need a few rapid fires to regain some sanity so please bare with us and thank you for your reports!!\n\n---\n\n## Resolved Issues\n\n* [Issue #1311](https://github.com/derailed/k9s/issues/1311) Pressing '?' in logs view (no logs) crashes on nil dereference\n* [Issue #1310](https://github.com/derailed/k9s/issues/1310) PV/PVC accessMode getting exception\n* [Issue #1293](https://github.com/derailed/k9s/issues/1293) Broken rollouts for dp/sts/ds with multiple ports of the same number\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.20.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.25.20\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated! Also big thanks to all that have allocated their own time to help others on both slack and this repo!!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release\n\nHoy!! Cleaning up the kitchen countertops ;( Thank you all for piping in on the latest drop!\n\n---\n\n## Resolved Issues\n\n* [Issue #1620](https://github.com/derailed/k9s/issues/1620) popeye view shows duplicate pdb\n* [Issue #1616](https://github.com/derailed/k9s/issues/1616) Age in nodes view are n/a\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.21.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.25.21\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated! Also big thanks to all that have allocated their own time to help others on both slack and this repo!!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release\n\n---\n\n## Resolved Issues\n\n* [Issue #1634](https://github.com/derailed/k9s/issues/1634) Namespace view all has the age field in strange format\n* [Issue #1633](https://github.com/derailed/k9s/issues/1633) Nodes sort by age has wrong order\n\n## Resolved PR\n\n* [PR #1632](https://github.com/derailed/k9s/pull/1632) Fix delete dialog dropdown styling\n* [PR #1629](https://github.com/derailed/k9s/pull/1629) Fix reference to base image in dockerfile\n* [PR #1627](https://github.com/derailed/k9s/pull/1627) Fix TestToAge\n* [PR #1624](https://github.com/derailed/k9s/pull/1624) Change makefile version\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.25.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\nAddressing broken windows builds ;(\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.4.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.25.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues\n\n* [Issue #1319](https://github.com/derailed/k9s/issues/1319) Namespace filters are no longer applied on startup\n* [Issue #1317](https://github.com/derailed/k9s/issues/1317) port forwarding broke with multiple exposed ports\n* [Issue #1316](https://github.com/derailed/k9s/issues/1316) Configuration for macOS is using wrong path\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.5.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.25.5\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues\n\n* [Issue #1327](https://github.com/derailed/k9s/issues/1327) Switching K8s resource changes view to all namespace\n* [Issue #1326](https://github.com/derailed/k9s/issues/1326) Port forwarding not possible because of \"invalid container port\"\n* [Issue #1325](https://github.com/derailed/k9s/issues/1325) Meaning of number in brackets after context name is unclear\n* [Issue #1324](https://github.com/derailed/k9s/issues/1324) Problem with Configuration for macOS is can't find configuration directory\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.6.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.25.6\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\nAnd the bit goes on...\n\n---\n\n## Resolved Issues\n\n* [Issue #1333](https://github.com/derailed/k9s/issues/1333) Log level not showing in k9s\n* [Issue #1253](https://github.com/derailed/k9s/issues/1253) Namespace filter automatically applied after viewing a deployment\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.7.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.25.7\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\nHappy (`Wild`) Turkey Day Everyone!!\n\n---\n\n## Resolved Issues\n\n* [Issue #1341](https://github.com/derailed/k9s/issues/1341) Colored container logs are not displayed correctly.\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.8.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.25.8\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues\n\n* [Issue #1349](https://github.com/derailed/k9s/issues/1349) Support events.k8s.io Event v1\n* [Issue #1345](https://github.com/derailed/k9s/issues/1345) Access denied after context switch\n* [Issue #1344](https://github.com/derailed/k9s/issues/1344) Use \"Port forward\",but \"invalid container port\"\n* [Issue #1342](https://github.com/derailed/k9s/issues/1342) Log screen refreshed every second\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2020 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.25.9.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s_small.png\" align=\"right\" width=\"200\" height=\"auto\"/>\n\n# Release v0.25.9\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n### A Word From Our Sponsors...\n\nI want to recognize the following folks that have been kind enough to join our sponsorship program and opted to `pay it forward`!\n\n* [Joshua Kapellen](https://github.com/joshuakapellen)\n* [Qdentity](https://github.com/qdentity)\n* [Maxim](https://github.com/bsod90)\n* [Sönke Schau](https://github.com/xgcssch)\n\nSo if you feel K9s is helping with your productivity while administering your Kubernetes clusters, please consider pitching in as it will go a long way in ensuring a thriving environment for this repo and our K9sers community at large.\n\nAlso please take some time and give a huge shoot out to all the good folks below that have spent time plowing thru the code to help improve K9s for all of us!\n\nThank you!!\n\n---\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues\n\n* [Issue #1361](https://github.com/derailed/k9s/issues/1361) Pulses not displaying graphs\n* [Issue #1358](https://github.com/derailed/k9s/issues/1358) Namespace list is empty\n* [Issue #1357](https://github.com/derailed/k9s/issues/1357) Benchmarks doesn't work on windows\n* [Issue #1355](https://github.com/derailed/k9s/issues/1355) Trace log level does not exist\n* [Issue #1345](https://github.com/derailed/k9s/issues/1345) Access denied after context switch\n\n---\n\n## PRs\n\n* [PR #1363](https://github.com/derailed/k9s/pull/1363) Add rose-pine skin.\n  [Sergio Soria](https://github.com/sasoria)\n* [PR #1356](https://github.com/derailed/k9s/pull/1356) Add flux trace shortcut to flux plugin.\n  [Guillaume Berche](https://github.com/gberche-orange)\n* [PR #1321](https://github.com/derailed/k9s/pull/1321) Add customizable dump directory property.\n  [Vlasov Artem](https://github.com/VlasovArtem)\n\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.26.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.26.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are as ever very much noted and appreciated! Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nIf you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\n* [Sugar Water - Cibo Matto](https://www.youtube.com/watch?v=EN9auBn6Jys)\n* [Midnight To Stevens - The Clash](https://www.youtube.com/watch?v=9suQJthS6to)\n* [Cool & Proper - Natty Nation](https://www.youtube.com/watch?v=9q337zn7bpI)\n\n---\n\n## Maintenance Release\n\nPlease join me in giving a big THANK YOU and ATTA BOY!! to [Aleksei Romanenko](https://github.com/slimus) for allocating his personal time in helping out his fellow K9sers with issues, PRs and slack!!\n\nAlso in the last drop, I'd updated k8s API's to the latest which caused some `disturbance in the farce!` and hosed AWS cluster connections in the same swop ;( Please see [Issue#119](https://github.com/derailed/k9s/issues/1619) for `a` resolve... I did not catch it early enough hence the release bump on this drop. My bad!!\n\n---\n\n## Resolved Issues\n\n* [Issue #1655](https://github.com/derailed/k9s/issues/1655) Text not appearing in context windows\n* [Issue #1654](https://github.com/derailed/k9s/issues/1654) K9s crash on m1 with index out of range [0] with length 0\n* [Issue #1652](https://github.com/derailed/k9s/issues/1652) HPA with custom metrics has \"Target%\" column showing \"unknown/unknown\"\n* [Issue #1639](https://github.com/derailed/k9s/issues/1639) Helm releases view broken after interacting with 0.25.21\n\n## Resolved PR\n\n* [PR #1656](https://github.com/derailed/k9s/pull/156) Fix PF and RS dialog colors\n* [PR #163](https://github.com/derailed/k9s/pull/1636) Fix #1636: can't switch context with --kubeconfig flag\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.26.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.26.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated! Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\nOldies but goodies...\n\n* [Love In Vain - Rolling Stones](https://www.youtube.com/watch?v=ryRDcE2sB2A)\n* [Old Love - Eric Clapton](https://www.youtube.com/watch?v=qv63M6XXgGE)\n* [Warm Weather - Pieces Of A Dream](https://www.youtube.com/watch?v=hYm6fR1Zjm4)\n* [Funerailles d'antan - George Brassens](https://www.youtube.com/watch?v=-mOalHzOCCM)\n\n---\n\n## A Word From Our Sponsors...\n\nTo all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!\n\n* [Jacky Nguyen](https://github.com/nktpro)\n* [Aleksei Romanenko](https://github.com/slimus)\n* [Aljoscha Pörtner](https://github.com/AljoschaP)\n* [Mario Bris](https://github.com/mariobris)\n* [Thorsten Schifferdecker](https://github.com/curx)\n* [Lungdart](https://github.com/lungdart)\n* [Azar](https://github.com/azarudeena)\n\n---\n\n## Maintenance Release\n\n---\n\n## Resolved Issues\n\n* [Issue #1684](https://github.com/derailed/k9s/issues/1684) Crash when viewing logs index out of range [2] with length 2\n* [Issue #1680](https://github.com/derailed/k9s/issues/1680) Changing to pod kill grace period from 0 to 1\n* [Issue #1661](https://github.com/derailed/k9s/issues/1661) ClusterRole with wrong privilege list display\n* [Issue #1677](https://github.com/derailed/k9s/issues/1677) UsedBy function on priorityclass\n* [Issue #1657](https://github.com/derailed/k9s/issues/1657) Cannot delete port forwarding created inside k9s\n* [Issue #1420](https://github.com/derailed/k9s/issues/1420) Unable to delete port forward\n\n## Resolved PR\n\n* [PR #1682](https://github.com/derailed/k9s/pull/1682) Fix: persistentvolumes not showing terminating status.\n* [PR #1672](https://github.com/derailed/k9s/pull/1672) Feat: allow to disable ctrl-c behavior\n* [PR #1666](https://github.com/derailed/k9s/pull/1666) Feat: show usedBy for priorityclasses\n* [PR #1668](https://github.com/derailed/k9s/pull/1668) Fix: PF delete with no container\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.26.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.26.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated! Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release\n\nDoh! Looks like I've broken windows on this last drop ;(\n\nNOTE: I currently don't have access to a windows/m1 box. So if you do please report back and help us zoom in on the issues below...\nThank you!!\n\n---\n\n## Resolved Issues (Wishfully...)\n\n* [Issue #1690](https://github.com/derailed/k9s/issues/1690) 0.26.1 stuck after exit from container shell, panels refreshed but arrow keys not works windows 10.\n* [Issue #1673](https://github.com/derailed/k9s/issues/1673) Screen goes blank after existing shell while running k9s on M1\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2022 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.26.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.26.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated! Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release\n\n---\n\n## Resolved Issues (Wishfully...)\n\n* [Issue #1690](https://github.com/derailed/k9s/issues/1690) 0.26.1 stuck after exit from container shell, panels refreshed but arrow keys not works windows 10.\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2022 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.26.4.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.26.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated! Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\n* [Love's Got Me High - Terrence Parker](https://www.youtube.com/watch?v=1KuLU6lpMT8)\n* [New money - Calvin Harris](https://www.youtube.com/watch?v=TUVw1PTO6Sc)\n* [Shrine - Jeff Beck](https://www.youtube.com/watch?v=-zBtluqp8l8)\n\n---\n\n## A Word From Our Sponsors...\n\nTo all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!\n\n* [Subshell](https://github.com/subshell)\n* [Dan Anglin](https://github.com/dananglin)\n* [Jacob Lorenzen](https://github.com/Jaxwood)\n* [Benjamin Herbert](https://github.com/BenjaminHerbert)\n* [Brandon G](https://github.com/gannicottb)\n* [Damyan Yordanov](https://github.com/damyan)\n* [Luiz Marques](https://github.com/luizfnunesmarques)\n* [Argonaut](https://github.com/argonautdev)\n* [Marcin Jasion](https://github.com/mjasion)\n\n---\n\n## Maintenance Release\n\n---\n\n## Resolved Issues\n\n* [Issue #1742](https://github.com/derailed/k9s/issues/1742) Edit and shell not working on Arch linux\n* [Issue #1724](https://github.com/derailed/k9s/issues/1724) redundant conversion exists\n* [Issue #1714](https://github.com/derailed/k9s/issues/1714) Cronjob: don't highlight changes in `last schedule`\n* [Issue #1711](https://github.com/derailed/k9s/issues/1711) Unable to see CRDs\n* [Issue #1700](https://github.com/derailed/k9s/issues/1700) Ctrl+D removes a pod instantly\n\n---\n\n## Contributed PRs (Thank you!!)\n\n* [PR #1759](https://github.com/derailed/k9s/pull/1759) Fix typo in cronjob\n* [PR #1755](https://github.com/derailed/k9s/pull/1755) List all helm releases by default\n* [PR #1753](https://github.com/derailed/k9s/pull/1753) Fix flux plugin to properly handle trace\n* [PR #1744](https://github.com/derailed/k9s/pull/1744) README: correct (auto-)port-forwards annotations\n* [PR #1739](https://github.com/derailed/k9s/pull/1739) Fix GracePeriodSeconds\n* [PR #1725](https://github.com/derailed/k9s/pull/1725) fix redundant type conversion code\n* [PR #1721](https://github.com/derailed/k9s/pull/1721) Replace keyboard package\n* [PR #1711](https://github.com/derailed/k9s/pull/1711) Fix get CustomResourceDefinition\n* [PR #1709](https://github.com/derailed/k9s/pull/1709) Plugin for opening a root shell to k3d container\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2022 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.26.5.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.26.5\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated! Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release\n\nSo it looks like replacing the clipboard package was indeed a dud ;(\nWhile I was not keen on either running with cgo or taking on external dependencies, after further investigation it looks like the clipboard + wsl issue in the old package was [resolved](https://github.com/atotto/clipboard/pull/42). I don't run WSL so I can't test it but if that's not the case please reopen and we will figure out another solution. For the time being, I've opted for the reversal.\nThank you!!\n\n---\n\n## Resolved Issues\n\n* [Issue #1742](https://github.com/derailed/k9s/issues/1770) copy to clipboard throw panic error\n* [Issue #1768](https://github.com/derailed/k9s/issues/1768) build fails due to new clipboard package\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2022 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.26.6.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.26.6\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated! Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release\n\n---\n\n## Resolved Issues\n\n* [Issue #1773](https://github.com/derailed/k9s/issues/1773) CustomResourceDefinition does not display\n\n## Contributed PRs (Thank you!!)\n\n* [PR #1777](https://github.com/derailed/k9s/pull/1777) Fix directory path when viewing screendump\n* [PR #1776](https://github.com/derailed/k9s/pull/1776) Add a closing tag when showing timestamp in log view\n* [PR #1775](https://github.com/derailed/k9s/pull/1775) Log toggles: add a space after \"on\" in logs view\n* [PR #1772](https://github.com/derailed/k9s/pull/1772) docs: update homebrew installation note\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2022 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.26.7.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.26.7\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated! Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release\n\n---\n\n## A Word From Our Sponsors...\n\nTo all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!\n\n* [Microsoft](https://github.com/microsoft)\n* [Audun V. Nes](https://github.com/avnes)\n* [Marco Aurelio Caldas Miranda](https://github.com/macmiranda)\n* [Jon Waltom](https://github.com/jon-walton)\n* [Eckl, Máté](https://github.com/ecklm)\n* [Iguanasoft](https://github.com/iguanasoft)\n\n---\n\n## Resolved Issues\n\n* [Issue #1805](https://github.com/derailed/k9s/issues/1805) CronJobs: allow sorting by LAST_SCHEDULE\n\n## Contributed PRs (Thank you!!)\n\n* [PR #1804](https://github.com/derailed/k9s/pull/1804) Allow multiple port forwards\n* [PR #1797](https://github.com/derailed/k9s/pull/1797) README - use go install\n* [PR #1793](https://github.com/derailed/k9s/pull/1793) Update CronJob version to v1\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2022 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.27.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.27.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated! Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\nI'd like to dedicate this release to `Jeff Beck` one of my all time favorite musicians that sadly passed away this last week ;(\n\n* [The Pump - Jeff Beck](https://www.youtube.com/watch?v=xiDYrQp9wFQ)\n* [Brush With The Blues - Jeff Beck](https://www.youtube.com/watch?v=O640IGLjnfs)\n* [Cause We've Ended As Lovers - Jeff Beck](https://www.youtube.com/watch?v=VC02wGj5gPw)\n* [Where Were You - Jeff Beck](https://www.youtube.com/watch?v=howz7gVecjE)\n* [Rockabilly Set At Ronnie Scott](https://www.youtube.com/watch?v=_3aIEzXHBWw)\n\n---\n\n## A Word From Our Sponsors...\n\nTo all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!\n\n* [Vibin reddy](https://github.com/vibin)\n* [Maciek Albin](https://github.com/mckk)\n* [Dherraj Yennam](https://github.com/dyennam)\n* [Alan Ream](https://github.com/aream2006)\n* [djheap](https://github.com/djheap)\n* [MaterializeInc](https://github.com/MaterializeInc)\n* [Jeff Evans](https://github.com/jeff303)\n\n---\n\n## Resolved Issues\n\n* [Issue #1917](https://github.com/derailed/k9s/issues/1917) Crash on open single ingress from list\n* [Issue #1906](https://github.com/derailed/k9s/issues/1680) k9s exits silently if screenDumpDir cannot be created\n* [Issue #1661](https://github.com/derailed/k9s/issues/1661) ClusterRole with wrong privilege list display\n* [Issue #1680](https://github.com/derailed/k9s/issues/1680) Change pod kill grace period for 0 to 1\n\n## Contributed PRs\n\nPlease give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [PR #1910](https://github.com/derailed/k9s/pull/1910) Replace x86_64 to amd64 build\n* [PR #1877](https://github.com/derailed/k9s/pull/1877) Bug: portforward custom containers not showing\n* [PR #1874](https://github.com/derailed/k9s/pull/1874) Feat: Add noLatestRevCheck config option\n* [PR #1872](https://github.com/derailed/k9s/pull/1872) Docs: Add k8s client compatibility matrix\n* [PR #1871](https://github.com/derailed/k9s/pull/1871) Bug: update scanSA calls to account for blank service accounts\n* [PR #1866](https://github.com/derailed/k9s/pull/1866) Bug: Fix order of arguments for CanI function call\n* [PR #1859](https://github.com/derailed/k9s/pull/1859) FEAT: Add vim-like quit force option\n* [PR #1849](https://github.com/derailed/k9s/pull/1849) Bug: Fix build date for OSX\n* [PR #1847](https://github.com/derailed/k9s/pull/1847) FEAT: Add labels configuration for shell node pod\n* [PR #1840](https://github.com/derailed/k9s/pull/1840) FEAT: Add policy view to service accounts\n* [PR #1837](https://github.com/derailed/k9s/pull/1837) FEAT: Use default terminal colors for better readability\n* [PR #1830](https://github.com/derailed/k9s/pull/1830) FEAT: Plugin support for carvel kapp CR\n* [PR #1829](https://github.com/derailed/k9s/pull/1829) FEAT: flux.yml plugin new displays stderr messages\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2022 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.27.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.27.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated! Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release\n\n---\n\n## Resolved Issues\n\n* [Issue #1943](https://github.com/derailed/k9s/issues/1943) k9s display is broken after switching to v0.27.0\n* [Issue #1935](https://github.com/derailed/k9s/issues/1935) Active namespace is dropped after accessing forbidden resources\n* [Issue #1913](https://github.com/derailed/k9s/issues/1913) Exit edit mode deadlock\n* [Issue #1895](https://github.com/derailed/k9s/issues/1895) AWS workspace. K9s fails on startup with unknown userid error\n* [Issue #1842](https://github.com/derailed/k9s/issues/1842) Strange one - brew installed k9s\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2022 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.27.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.27.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated! Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release\n\nWith feelings... Broke brew installer ;(\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2022 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.27.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.27.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated! Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\n* [Bitches Brew - Miles Davis](https://www.youtube.com/watch?v=50fB5L1vmn8)\n* [Sordid Affair - Röyksopp](https://www.youtube.com/watch?v=ECL5zO6ImsA)\n* [Love Inc - Booka Shade](https://www.youtube.com/watch?v=sgLxTcok8kQ)\n* [Twisted - Kaz James,Nick Morgan](https://www.youtube.com/watch?v=oOsYJ-Co8Y4)\n\n---\n\n## A Word From Our Sponsors...\n\nTo all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!\n\n* [Astraea](https://github.com/s22)\n* [Arnaud Bienvenu](https://github.com/abienvenu)\n* [Eric Caleb](https://github.com/iamcaleberic)\n* [Sean Williams](https://github.com/SeanThomasWilliams)\n* [Federico Ragona](https://github.com/fedragon)\n\n> Sponsorship cancellations since the last release: `7` ;(\n\n---\n\n## Maintenance Release\n\n---\n\n## Resolved Issues\n\n* [Issue #1968](https://github.com/derailed/k9s/issues/1968) Some skins are missing the definitions for the help menu\n* [Issue #1967](https://github.com/derailed/k9s/issues/1967) Helm cve-2023-25165\n* [Issue #1964](https://github.com/derailed/k9s/issues/1964) logger.sinceSeconds config setting inconsistent with README\n* [Issue #1955](https://github.com/derailed/k9s/issues/1955) K9s crashes with empty resources and/or verbs in RBAC\n* [Issue #1954](https://github.com/derailed/k9s/issues/1954) Open very slow\n* [Issue #1883](https://github.com/derailed/k9s/issues/1883) Fix force deletion\n* [Issue #1788](https://github.com/derailed/k9s/issues/1788) Draining nodes cannot be forced\n* [Issue #1150](https://github.com/derailed/k9s/issues/1150) Add a persistent popup for drain failures\n\n---\n\n## Contributed PRs\n\nPlease give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [PR #1969](https://github.com/derailed/k9s/pull/1969) fix: Add missing help menu to gruvbox-dark skin\n* [PR #1966](https://github.com/derailed/k9s/pull/1966) fix: Show meaningful error message when kubectl exec fails\n* [PR #1965](https://github.com/derailed/k9s/pull/1965) set default sinceSeconds to 300\n* [PR #1961](https://github.com/derailed/k9s/pull/1961) feat: Add sort by pod count on node view\n* [PR #1960](https://github.com/derailed/k9s/pull/1960) [Misc] Add Nightfox-theme\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2022 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.27.4.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.27.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated! Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n## Core Team...\n\nPlease help me welcome Aleksei Romanenko(https://github.com/slimus) to the K9s contributor team!!\nAlex is very knowledgeable in this space, kind and a great human being!\nHe has been instrumental with issues, prs and fielding questions in forums and slack.\n\n🎉 Welcome Alex!!🎉\n\n---\n\n## A Word From Our Sponsors...\n\nTo all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!\n\n* [Jon Walton](https://github.com/jon-walton)\n* [gmbnomis](https://github.com/gmbnomis)\n* [Alex Viscreanu](https://github.com/aexvir)\n* [Björn Petersen](https://github.com/BjoernPetersen)\n* [Tanner Watson](https://github.com/tannerwatson)\n* [Jabunovoty](https://github.com/jabunovoty)\n* [Joey Guerra](https://github.com/joeyguerra)\n* [Materialize Inc](https://github.com/MaterializeInc)\n* [Kijana Woodard](https://github.com/kijanawoodard)\n* [Tom Saleeba](https://github.com/tomsaleeba)\n* [William Alexander](https://github.com/carpetfuz)\n* [Süddeutsche Zeitung](https://github.com/sueddeutsche)\n\n> Sponsorship cancellations since the last release: `12` ;(\n\n---\n\n## Maintenance Release\n\n---\n\n## Resolved Issues\n\n* [Issue #2072](https://github.com/derailed/k9s/issues/2072) Triggered Job from cronjob is missing annotations\n* [Issue #2024](https://github.com/derailed/k9s/issues/2024) Allow customization of log indicators with skin theme\n* [Issue #1971](https://github.com/derailed/k9s/issues/1971) Zip binary for windows\n\n---\n\n## Contributed PRs\n\nPlease give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [PR #2073](https://github.com/derailed/k9s/pull/2073) Fix for missing Job annotations created from Cronjob\n* [PR #2069](https://github.com/derailed/k9s/pull/2069) Unify all go version to 1.20\n* [PR #2067](https://github.com/derailed/k9s/pull/2067) Create narsingh skin\n* [PR #2054](https://github.com/derailed/k9s/pull/2054) Update setup-go action, with caching\n* [PR #2045](https://github.com/derailed/k9s/pull/2045) Fix: (views) use saved context view when switching\n* [PR #2041](https://github.com/derailed/k9s/pull/2041) Feat: allow customization of log indicator toggles\n* [PR #2030](https://github.com/derailed/k9s/pull/2030) Updated monokai skin with help styles, and more monokai appropriate colors\n* [PR #2027](https://github.com/derailed/k9s/pull/2027) Roles are rendered using same colorer function from skin\n* [PR #2045](https://github.com/derailed/k9s/pull/2045) Fix: (views) use saved context view when switching\\\n* [PR #2011](https://github.com/derailed/k9s/pull/2011) Fix #2007: Remove debug command\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.28.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.28.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated! Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\n* [Moonlight Invasions - TribalNeed](https://www.youtube.com/watch?v=mJBnMSNIJL4&list=RDmJBnMSNIJL4&start_radio=1)\n* [Teardrops - Neil Frances](https://www.youtube.com/watch?v=823_KoZr4mo)\n* [Memory - Øystein Sevåg](https://www.youtube.com/watch?v=GKEM6lgkogY)\n* [Tell me straight - Rolling Stones (Generated by KeithGPT 🐭)](https://www.youtube.com/watch?v=YxcxLi-Ld3E)\n\n---\n\n## A Word From Our Sponsors...\n\nTo all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!\n\n* [Hyeon Woo Jo](https://github.com/dokdo2013)\n* [Artsiom Kaval](https://github.com/lezeroq)\n* [Grant Linville](https://github.com/g-linville)\n* [Andrew Brown](https://github.com/andrew-werdna)\n* [Patrik Votoček](https://github.com/Vrtak-CZ)\n* [Erik Hebisch](https://github.com/flegelleicht)\n* [Juliet Boyd](https://github.com/julietrb1)\n* [Chris Vertonghen](https://github.com/chrisv)\n* [Acsone](https://github.com/acsone)\n* [Alex Viscreanu](https://github.com/aexvir)\n* [Joey Guerra](https://github.com/joeyguerra)\n* [Kijana Woodard](https://github.com/kijanawoodard)\n* [Tom Saleeba](https://github.com/tomsaleeba)\n\n> Sponsorship cancellations since the last release: `11` ;(\n\n---\n\n## Feature Release\n\n### File Transfers in Da House!\n\nAdded ability to exchange files from your local machine to a pod or from a pod to your local machine. The pod view now surfaces a new command `t` to initiate the download/upload file transfers.\n\n---\n\n## Resolved Issues\n\n* [Issue #2249](https://github.com/derailed/k9s/issues/2249) Sort on the capacity column should consider Gi and Mb also\n* [Issue #2225](https://github.com/derailed/k9s/issues/2225) View logs of all pods of a given deployment\n* [Issue #2195](https://github.com/derailed/k9s/issues/2195) Some pod logs are not displayed. But I can display it when I use the command\n\n* [Issue #2194](https://github.com/derailed/k9s/issues/2194) 0.27.4 broke custom sort orders via views.yml\n* [Issue #2185](https://github.com/derailed/k9s/issues/2185) No binaries for Linux_x86_64\n* [Issue #2169](https://github.com/derailed/k9s/issues/2169) Add namespace name in ServiceAccount view with RoleBinding\n* [Issue #2152](https://github.com/derailed/k9s/issues/2152) Latest opened namespace not being saved between k9s sessions\n* [Issue #2131](https://github.com/derailed/k9s/issues/2131) deployments are not showing up, whereas kubectl gives a list\n* [Issue #2130](https://github.com/derailed/k9s/issues/2130) Pending pods show 0/0 Ready instead of 0/x Ready\n* [Issue #2128](https://github.com/derailed/k9s/issues/2128) k9s command not found after snap install\n* [Issue #2121](https://github.com/derailed/k9s/issues/2121) colors for crds\n* [Issue #2120](https://github.com/derailed/k9s/issues/2120) kustomize deletion not working as expected\n* [Issue #2106](https://github.com/derailed/k9s/issues/2106) k9s delete behaves differently with kubectl\n* [Issue #2085](https://github.com/derailed/k9s/issues/2085) When specifying the context command via the -c flag, selecting a cluster always returns to the context view\n* [Issue #658](https://github.com/derailed/k9s/issues/658) Feature request: Easy way to copy/download files from a pod/pv to your local PC\n\n---\n\n## Contributed PRs\n\nPlease give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [PR #2258](https://github.com/derailed/k9s/pull/2258) fix fsnotify watcher not fully working\n* [PR #2253](https://github.com/derailed/k9s/pull/2253) fix manual sorting not working when sortColumn is configured\n* [PR #2252](https://github.com/derailed/k9s/pull/2252) consider units when sorting capacity of pv and pvc\n* [PR #2243](https://github.com/derailed/k9s/pull/2243) fix(typo): pdb header typo\n* [PR #2239](https://github.com/derailed/k9s/pull/2239) fix: honor defaults from drain dialog in request\n* [PR #2235](https://github.com/derailed/k9s/pull/2235) docs: add plugin.yml JSON schema\n* [PR #2229](https://github.com/derailed/k9s/pull/2229) fix(log): clear bold log format after timestamp\n* [PR #2188](https://github.com/derailed/k9s/pull/2188) Alias qa to quit\n* [PR #2180](https://github.com/derailed/k9s/pull/2180) feat: Added support for arm in dockerfile\n* [PR #2179](https://github.com/derailed/k9s/pull/2179) Focus command bar if active on startup\n* [PR #2170](https://github.com/derailed/k9s/pull/2170) Add namespace for rolebinding on a clusterrole\n* [PR #2161](https://github.com/derailed/k9s/pull/2161) Only apply keyConv to mnemonic in menus\n* [PR #2158](https://github.com/derailed/k9s/pull/2158) Show the default container as the first entry\n* [PR #2153](https://github.com/derailed/k9s/pull/2153) Changed checksums extension to checksums.sha256\n* [PR #2158](https://github.com/derailed/k9s/pull/2158) Show the default container as the first entry\n* [PR #2151](https://github.com/derailed/k9s/pull/2151) chore: pkg imported more than once\n* [PR #2147](https://github.com/derailed/k9s/pull/2147) feat: plugin for adding an ephemeral debug container\n* [PR #2141](https://github.com/derailed/k9s/pull/2141) Update plugin flux.yml with shortcuts for helm repo and oci repos\n* [PR #2137](https://github.com/derailed/k9s/pull/2137) Correctly display the numbers in the Ready column of the pods view\n* [PR #2136](https://github.com/derailed/k9s/pull/2136) Prompt window uses border styles\n* [PR #2134](https://github.com/derailed/k9s/pull/2134) Remove unsupported key binding on users view\n* [PR #2124](https://github.com/derailed/k9s/pull/2124) fix: add correct flags when deleting resources from Dir\n* [PR #2119](https://github.com/derailed/k9s/pull/2119) feat: add indicator to title if toast is toggled\n* [PR #2117](https://github.com/derailed/k9s/pull/2117) Add instruction how to install k9s through winget\n* [PR #2112](https://github.com/derailed/k9s/pull/2112) Fix for styles\n* [PR #2105](https://github.com/derailed/k9s/pull/2105) Fix the wrong/redundant icon in the prompt bar\n* [PR #2103](https://github.com/derailed/k9s/pull/2103) Update carvel.yml to include contexts\n* [PR #2096](https://github.com/derailed/k9s/pull/2096) fix: (config) only respect the --command flag once\n* [PR #2091](https://github.com/derailed/k9s/pull/2091) Add get-all plugin specific for namespace view\n* [PR #2089](https://github.com/derailed/k9s/pull/2089) Resources are rendered using skin.yaml colors\n* [PR #2082](https://github.com/derailed/k9s/pull/2082) Fix typo introduced in #2045\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.28.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.28.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated! Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\n* [If Trouble Was Money - Albert Collins](https://www.youtube.com/watch?v=cz6LbWWqX-g)\n* [Old Love - Eric Clapton](https://www.youtube.com/watch?v=EklciRHZnUQ)\n* [Touch And GO - The Cars](https://www.youtube.com/watch?v=L7Gpr_Auz8Y)\n\n---\n\n## A Word From Our Sponsors...\n\nTo all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!\n\n* [Bradley Heilbrun](https://github.com/bheilbrun)\n\n> Sponsorship cancellations since the last release: `2` ;(\n\n---\n\n## Feature Release\n\n### Sanitize Me!\n\nOver time, you might end up with a lot of pod cruft on your cluster. Pods that might be completed, erroring out, etc... Once you've completed your pod analysis it could be useful to clear out these pods from your cluster.\n\nIn this drop, we introduce a new command `sanitize` aka `z` available on pod views otherwise known as `The Axe!`. This command performs a clean up of all pods that are in either in completed, crashloopBackoff or failed state. This could be especially handy if you run workflows jobs or commands on your cluster that might leave lots of `turd` pods. Tho this has a `phat` fail safe dialog please be careful with this one as it is a blunt tool!\n\n---\n\n## Resolved Issues\n\n* [Issue #2281](https://github.com/derailed/k9s/issues/2281) Can't run Node shell\n* [Issue #2277](https://github.com/derailed/k9s/issues/2277) bulk actions applied to power filters\n* [Issue #2273](https://github.com/derailed/k9s/issues/2273) Error when draining node that is cordoned bug\n* [Issue #2233](https://github.com/derailed/k9s/issues/2233) Invalid port-forwarding status displayed over the k9s UI\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [PR #2280](https://github.com/derailed/k9s/pull/2280) chore: replace github.com/ghodss/yaml with sigs.k8s.\n* [PR #2278](https://github.com/derailed/k9s/pull/2278) README.md: fix typo in netshoot URL\n* [PR #2275](https://github.com/derailed/k9s/pull/2275) check if the Node already cordoned when executing Drain\n* [PR #2247](https://github.com/derailed/k9s/pull/2247) Delete port forwards when pods get deleted\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.28.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.28.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s! I'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev and see if we're happier with some of the fixes! If you've filed an issue please help me verify and close. Your support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated! Also big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\n* [If Trouble Was Money - Albert Collins](https://www.youtube.com/watch?v=cz6LbWWqX-g)\n* [Old Love - Eric Clapton](https://www.youtube.com/watch?v=EklciRHZnUQ)\n* [Touch And GO - The Cars](https://www.youtube.com/watch?v=L7Gpr_Auz8Y)\n\n---\n\n## A Word From Our Sponsors...\n\nTo all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!\n\n* [Bradley Heilbrun](https://github.com/bheilbrun)\n\n> Sponsorship cancellations since the last release: `2` ;(\n\n---\n\n## Feature Release\n\n### Sanitize Me!\n\nOver time, you might end up with a lot of pod cruft on your cluster. Pods that might be completed, erroring out, etc... Once you've completed your pod analysis it could be useful to clear out these pods from your cluster.\n\nIn this drop, we introduce a new command `sanitize` aka `z` available on pod views otherwise known as `The Axe!`. This command performs a clean up of all pods that are in either in completed, crashloopBackoff or failed state. This could be especially handy if you run workflows jobs or commands on your cluster that might leave lots of `turd` pods. Tho this has a `phat` fail safe dialog please be careful with this one as it is a blunt tool!\n\n---\n\n## Resolved Issues\n\n* [Issue #2281](https://github.com/derailed/k9s/issues/2281) Can't run Node shell\n* [Issue #2277](https://github.com/derailed/k9s/issues/2277) bulk actions applied to power filters\n* [Issue #2273](https://github.com/derailed/k9s/issues/2273) Error when draining node that is cordoned bug\n* [Issue #2233](https://github.com/derailed/k9s/issues/2233) Invalid port-forwarding status displayed over the k9s UI\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [PR #2280](https://github.com/derailed/k9s/pull/2280) chore: replace github.com/ghodss/yaml with sigs.k8s.\n* [PR #2278](https://github.com/derailed/k9s/pull/2278) README.md: fix typo in netshoot URL\n* [PR #2275](https://github.com/derailed/k9s/pull/2275) check if the Node already cordoned when executing Drain\n* [PR #2247](https://github.com/derailed/k9s/pull/2247) Delete port forwards when pods get deleted\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.29.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.29.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\n* [Snowbound - Donald Fagen](https://www.youtube.com/watch?v=bj8ZdBdKsfo)\n* [Pilgrim - Eric Clapton](https://www.youtube.com/watch?v=8V9tSQuIzbQ)\n* [Lucky Number - Lene Lovich](https://www.youtube.com/watch?v=KnIJOO__jVo)\n\n---\n\n## 🦃 Happy (Belated!) ThanksGiving To All! 🦃\n\nHope you and yours had a wonderful holiday!!\nHopefully this drop won't be a cold turkey 😳\n\nI'd like to take this opportunity to honor two very special folks:\n\n* [Alexandru Placinta](https://github.com/placintaalexandru)\n* [Jayson Wang](https://github.com/wjiec)\n\nThese guys have been relentless in fishing out bugs, helping out with support and addressing issues, not to mention enduring my code! 🙀\nThey dedicate a lot of their time to make `k9s` better for all of us!\nSo if you happen to run into them live/virtual, please be sure to `Thank` them and give them a huge hug! 🤗\n\nI am thankful for all of you for being kind, patient, understanding and one of the coolest OSS community on the web!!\n\nFeeling blessed and ever so humbled to be part of it.\n\nThank you!!\n\n---\n\n## A Word From Our Sponsors...\n\nTo all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!\n\n* [Marco Stuurman](https://github.com/fe-ax)\n* [Paul Sweeney](https://github.com/Kolossi)\n* [Cayla Fauver](https://github.com/cayla)\n* [alemanek](https://github.com/alemanek)\n* [Danske Commodities A/S](https://github.com/DanskeCommodities)\n\n> Sponsorship cancellations since the last release: **8** ;(\n\n---\n\n## 🎉 Feature Release 🎈👯\n\n---\n\n### Breaking Bad!\n\nWARNING! There are breaking change on this drop!\n\n1. NodeShell configuration has moved up in the k9s config file from the context section to the top level config.\nMore than likely, one uses the same nodeShell image with all the fixins to introspect nodes no matter the cluster. This update DRY's up k9s config and still allows one to opt in/out of nodeShell via the context specific feature gate.\nPlease see README for the details.\n\n   > NOTE: If you haven't customize the shellPod images on your contexts, the app will move the nodeShell config section to\n   > it's new location and update your clusters information accordingly.\n   > If not, you will need to edit the nodeShell section and manage it from a single location!\n\n1. Log view used to default to the last 5mins aka `sinceSeconds: 300`.\n   Changed the default to tail logs instead aka `sinceSeconds: -1`\n\n1. Skins loading changed! In this release, we do away with the context specific skin files. You can now directly specify the skin to use for a given cluster directly in the k9s config file under the cluster configuration. K9s now expects a skins directory in the k9s config home with your skin files. You can use your custom skins and copy them to the `skins` directory or use the contributes skins found on this repo root.\nSpecify the name of the skin in the config file and now your cluster will load the specified skin.\n\nFor example: create a `skins` dir your k9s config home and add one_dark.yml skin file from this repo. Then edit your k9s config file as follows:\n\n```yaml\nk9s:\n  ...\n  clusters:\n    fred:\n      # Override the default skin and use this skin for this cluster.\n      skin: one_dark # -> Look for a skin file in ~/.config/k9s/skins/one_dark.yml\n      namespace:\n        ...\n      view:\n        active: pod\n      featureGates:\n        nodeShell: false\n      portForwardAddress: localhost\n```\n\nThe `fred` cluster will now load with the specified skin name. Rinse and repeat for other clusters of your liking. In the case where neither the skin dir or skin file are present, k9s will still honor the global skin aka `skin.yml` in your k9s config home directory to skin all your clusters.\n\n---\n\n### Walk Of SHelm...\n\nAdded a `Releases` view to Helm!\n\nThis provides the ability for Helm users to manage their releases directly from k9s.\nYou can now press `enter` on a selected Helm install and view all associated releases.\nWhile in the releases view, you can also rollback an install to a previous revision.\n\n---\n\n### Spock! Are You Out Of Your VulScan Mind?\n\nTired of having malignant folks shoot holes in your prod clusters or failing compliance testing?\n\nAdded ability to run image vulnerability scans directly from k9s. You can now monitor your security stance in dev/staging/... clusters\nprior to proclaiming `It's Open Season...` in prod!\n\nAs it stands Pod, Deployment, StatefulSet, DaemonSet, CronJob, Job views will feature a new column for Vulnerability Scan aka `VS`.\n\n> NOTE! This feature is gated so you'll need to manually opt in/out by modifying your k9s config file like so:\n\n```yaml\nk9s:\n  liveViewAutoRefresh: false\n  enableImageScan: true # <- Yes Please!!\n  headless: false\n  ...\n```\n\nOnce enabled, a new column `VS` (aka Vulnerability Score) should be present on the aforementioned views where you will see your vulnerability scores (*Still work in progress!!*).\nThe `VS` column displays a bit vector aka Sev-1|Sev-2|Sev-3|Sev-4|Sev-5|Sev-Unknown. When the bit is high it indicate the presence of the severity in the scans. Higher order bits = Higher severity\nFor instance, the following vector `110001` indicates the presence of both critical (Sev-1) and high (Sev-2) and an unclassified severity (aka Sev-Unknown) issues in the scan. Sev-U indicates no classification currently exist in our vulnerability database.\n\nThe image scans are run async, rendering the views eventually consistent, hence you may have to give the scores a few cycles for the dust to settle...\nOnce the caches are primed, subsequent loads should be faster 🤞\n\nYou can sort the views by vulnerability score using `ShiftV`.\nAdditionally, you can view the full scans report by pressing `v` on a selected resource.\n\nI've synced my entire Thanksgiving holiday break on this ding dang deal, so hopefully it works for most of you??\nAlso if you dig this new feature, please make some noise! 😍\n\n💘 This is an experimental feature and likely will require additional TLC 💘\n\n> NOTE! The lib we use to scan for vulnerabilities only supports macOS and Linux!!\n> NOTE: I have yet to test this feature on larger clusters, so likely this may break??\n> Please take these reports with a grain of salt as likely your mileage will vary and help us\n> validate the accuracy of the report ie if we cry `Wolf`, is it actually there?\n\nThe paint is still fresh on this deal!!\n\n### Do You Tube?\n\nMy plan is to begin (again!) putting out short k9s episodes with how-tos, tips, tricks and features previews.\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\nThe first drop should be up by the time you read this!\n\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2308](https://github.com/derailed/k9s/issues/2308) Unable to list CRs for crd with only list and get verb without watch verb\n* [#2301](https://github.com/derailed/k9s/issues/2301) Add imagePullPolicy and imagePullSecrets on shell_pod for internal registry uses\n* [#2298](https://github.com/derailed/k9s/issues/2298) Weird color after plugin usage\n* [#2297](https://github.com/derailed/k9s/issues/2297) Select nodes with space does not work anymore\n* [#2290](https://github.com/derailed/k9s/issues/2290) Provide release assets for freebsd amd64/arm64\n* [#2283](https://github.com/derailed/k9s/issues/2283) Adding auto complete in search bar\n* [#2219](https://github.com/derailed/k9s/issues/2219) Add tty: true to the node shell pod manifest\n* [#2167](https://github.com/derailed/k9s/issues/2167) Show wrong Configmap data\n* [#2166](https://github.com/derailed/k9s/issues/2166) Taint count for the nodes view\n* [#2165](https://github.com/derailed/k9s/issues/2165) Restart counter for init containers\n* [#2162](https://github.com/derailed/k9s/issues/2162) Make edit work when describing a resource\n* [#2154](https://github.com/derailed/k9s/issues/2154) Help and h command does not work if typed into cmdbuff\n* [#2036](https://github.com/derailed/k9s/issues/2036) Crashed while do filtering\n* [#2009](https://github.com/derailed/k9s/issues/2009) Ctrl-s: Name of file (Describe-....)\n* [#1513](https://github.com/derailed/k9s/issues/1513) Problem regarding showing the logs - it hangs/slow on pods which are running for long time\n  NOTE: Better but not cured! Perf improvements while viewing large cm (7k lines) from 26s->9s\n* [#568](https://github.com/derailed/k9s/issues/568) Allow both .yaml and .yml yaml config files\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#2322](https://github.com/derailed/k9s/pull/2322) Check if the service provides selectors\n* [#2319](https://github.com/derailed/k9s/pull/2319) Proper handling of help commands (fixes #2154)\n* [#2315](https://github.com/derailed/k9s/pull/2315) Fix namespace suggestion error on context switch\n* [#2313](https://github.com/derailed/k9s/pull/2313) Should not clear screen when executing plugin command\n* [#2310](https://github.com/derailed/k9s/pull/2310) chore: Mot recommended to use k8s.io/kubernetes as a dependency\n* [#2303](https://github.com/derailed/k9s/pull/2303) Clean up items\n* [#2301](https://github.com/derailed/k9s/pull/2301) feat: Add imagePullSecrets and imagePullPolicy configuration for shellpod\n* [#2289](https://github.com/derailed/k9s/pull/2289) Clean up issues introduced in #2125\n* [#2288](https://github.com/derailed/k9s/pull/2288) Fix merge issues from PR #2168\n* [#2284](https://github.com/derailed/k9s/issues/2284) Allow both .yaml and .yml yaml config files\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.29.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.29.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## Maintenance Release\n\n---\n\n## Resolved Issues\n\n* [#2330](https://github.com/derailed/k9s/issues/2330) Skins don't work v0.29.0\n* [#2329](https://github.com/derailed/k9s/issues/2329) New skin system in v0.29.0 doesn't work if you use different k8s context files\n* [#2327](https://github.com/derailed/k9s/issues/2327) [Bug] Item highlighting broke in v0.29.0\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.30.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s-xmas.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.30.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\nGoing back to the classics...\n\n* [Home For Christmas - Fats Domino](https://www.youtube.com/watch?v=ykAVdPz8o1Q)\n* [Our Love - Al Jarreau](https://www.youtube.com/watch?v=9ztMe6GIwi8)\n* [Body And Soul - Louis Armstrong](https://www.youtube.com/watch?v=2Gnz69TbqHQ)\n* [On The Dunes - Donald Fagen](https://www.youtube.com/watch?v=QoVT3XcMVvk)\n* [Ciao - Lucio Dalla](https://www.youtube.com/watch?v=qcqXcmKu_I4)\n* [Basin Street Blues - Louis Prima](https://www.youtube.com/watch?v=IijXXXpUefM&list=RDIijXXXpUefM&start_radio=1)\n\n---\n\n## A Word From Our Sponsors...\n\nTo all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!\n\n* [Bojan](https://github.com/rbojan)\n\n> Sponsorship cancellations since the last release: **5!** 🥹\n\n---\n\n## 🎄 Feature Release! 🎄\n\n🎅 Merry Christmas to all and Best wishes for the new year!!🧑‍🎄\n\n---\n\n### Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n### Breaking Bad!\n\n> ☢️ !!Prior to installing v0.30.0!! Please be sure to backup your k9s configs directories or move them somewhere safe!!\n\n> ☢️ Please watch the v0.30.0 Sneak peek series (links below) for detailed information.\n>\n> ☢️ Most K9s configuration files have either split or changed location or names on this drop!!\n\n> We recommend moving your current k9s config dirs to another location and start k9s from scratch and let it create and initialize the various configs\n> to their new spec and location. You can then use your existing setup and patch with the new layout/spec.\n> As of v0.30.0 all config files now use the `*.yaml` extension. We did our best to update all the docs to match the new version.\n> If you find doc issues either file an issue or better yet submit a PR!\n\nSome of you might say: `You're on the roll their bud! Two breaking changes drops in a row!!`\nPer the wise words of my beloved Grand mama! `One can't cook a decent meal without creating a mess!`\nNot to mention we're still at v0.x.y so `Open season on breaking changes` is very much in full effect.\n\nTho I have tested this drop quite a bit, there is a strong chance that I've broken some stuff.\nThe key here is to walk the fine line of improving k9s code base and features set with minimal impact to you.\nAs you know by now, I am committed to ease the pain and resolve issues quickly to get you all back up and running.\n\nFrom the scope changes in this release, I would caution that this drop will likely break you!\nIf so, worry not! We will fix the duds so we are `Happy as a Hippo` once again.\n\nThere was a few issues with the way K9s persists it's configuration and various artifacts. So we rewrote it!\nFirst and foremost all k9s related YAML resources, will now use the standard \".yaml\" extension.\nI think we've bloated the code checking for both extensions with no real actionable value!\n\nAs it stands the main K9s configuration `config.yml` will now be static. These settings are now readonly! All the dynamic configurations that K9s manages now live in a new directory aka `clusters`. The clusters directory manages your k8s cluster/context configurations. So things like active view, namespace, favorites, etc... now live in this directory. K9s configurations are still managed using either xdg `XDG_CONFIG_HOME` or you can set `K9S_CONFIG_DIR` to specify your preferred k9s configs location. Also all config files will now use the \".yaml\" extension vs \".yml\"!!\n\nSo the main k9s configuration (static) now looks like this:\n\n```yaml\n# $XDG_CONFIG_HOME/k9s/config.yaml\n# File will be autogenerated with all the default fixins if not found in the config specification.\nk9s:\n  liveViewAutoRefresh: false\n  refreshRate: 2\n  maxConnRetry: 5\n  readOnly: false\n  noExitOnCtrlC: false\n  ui: # NOTE! New level!!\n    enableMouse: false\n    headless: false\n    logoless: false\n    crumbsless: false\n    noIcons: false\n  skipLatestRevCheck: false\n  disablePodCounting: false\n  # ShellPod configuration applies to all your clusters\n  shellPod:\n    image: busybox:1.35.0\n    namespace: default\n    limits:\n      cpu: 100m\n      memory: 100Mi\n  # ImageScan config changed from v0.29.0!\n  imageScans:\n    enable: false\n    # Now figures exclusions ie excludes certain namespaces or specific workload labels\n    exclusions:\n      # Exclude the following namespaces for image vulscans!\n      namespaces:\n        - kube-system\n        - fred\n      # Exclude the following labels from image vulscans!\n      labels:\n        k8s-app:\n          - kindnet\n          - bozo\n        env:\n          - dev\n  logger:\n    tail: 100\n    buffer: 5000\n    sinceSeconds: -1\n    fullScreenLogs: false\n    textWrap: false\n    showTime: false\n  thresholds:\n    cpu:\n      critical: 90\n      warn: 70\n    memory:\n      critical: 90\n      warn: 70\n```\n\nNext context specific configurations that are managed by you and k9s live in the XDG data directory\ni.e `$XDG_DATA_HOME/k9s/clusters` or `$K9S_CONFIG_DIR/clusters` if the env var is set.\n\n```text\n$XDG_DATA_HOME/k9s\n// Clusters tracks visited kubeconfig cluster/contexts\n├── clusters\n│   ├── fred\n│   │   └── bozo\n│   │       └── config.yaml\n│   ├── bozorg\n│   │   ├── kind-bozo-1\n│   │   │   └── config.yaml\n│   │   ├── kind-bozo-2\n│   │   │   └── config.yaml\n│   │   └── kind-bozo-3\n│   │       └── config.yaml\n│   └── bumblebeetuna\n│       └── blee\n│           └── config.yaml\n└── skins\n    ├── black_and_wtf.yaml\n    ├── dracula.yaml\n    ├── in_the_navy.yml\n    ├── ...\n```\n\nNow looking at a given context configuration i.e cluster-1/context-1/config.yaml\n\n```yaml\n# $XDG_DATA_HOME/k9s/clusters/bumblebeetuna/blee/config.yaml\nk9s:\n  cluster: bumblebeetuna\n  readOnly: false # [New!] you can now single out a given context and make it readonly. Woof!\n  skin: in_the_navy # [NEW!] you can also skin individual contexts. Woof Woof!\n  namespace:\n    active: all\n    lockFavorites: false\n    favorites:\n    - all\n    - kube-system\n    - default\n  view:\n    active: dp\n  featureGates:\n    nodeShell: false\n  portForwardAddress: localhost\n```\n\nTransient artifacts ie k9s logs, screen-dumps, benchmarks etc now live in the state config dir.\n\n```text\n$XDG_STATE_HOME/k9s\n├── k9s.log # K9s log files\n└── screen-dumps\n    └── bumblebeetuna # Screen dumps location for context blee\n        └── blee\n            └── deployments-kube-system-1703018199222861000.csv\n```\n\nIf you get stuck or if my instructions are just `clear as mud`... `k9s info` is always your friend!!\n\nI feel this is an improvement (tho I might be unanimous on this!) especially for folks dealing with multi-clusters or swapping out there kubeconfigs...\n\n> NOTE! Paint is still fresh on this deal. Proceed with caution and please help us flush this feature out!\n\n---\n\n# Got Prompt?\n\nIn this drop, we've also given the k9s command prompt aka `:xxx` some love.\nYou have the ability to specify filter directly in the prompt.\n\nSo for example, you can now run something like `:po /fred` to run pod view with a filter to just show pods containing `fred`. Likewise `:po k8s-app=fred,env=blee` to filter by labels.\nAnd now for the`Krampus` special... you can see pods in a different context all together via `:pod @ctx-2`.\nFinally you can combo and send the `whole enchilada` via `:po k8s-app=fred /blee ns-1 @ctx-x`\nDid I mention with completion where applicable? Yes Please!!\nCompliments of [Jayson Wang](https://github.com/wjiec). Be sure to thank him!!\n\nPut these frequent flyers command in an alias and now you can nav your clusters with `even more style`!\n\n---\n\n# All Is Love?\n\n🎵 `On The twentieth day of Christmas my true love gave to me... Ten workloads a-leaping??...` 🎵\n\nThis is a feature reported by many of you and its (finally!) here. As of this drop, we intro the `workload` view aka `wk` which is similar to `kubectl get all`. I was reluctant to intro it given the potential hazards on larger clusters but figured why not? YOLO. I think using it in combo with the prompt updates it could pack a serious punch to observe workload related artifacts.\n\n---\n\n# Vulnerability Scan Exclusions...\n\nAs it seems customary with all k9s new features, folks want to turn them off ;(\nThe `Vulscan` feature did not get out unscathed ;(\nAs it was rightfully so pointed out, you may want to opted out scans for images that you do not control.\nTho I think it might be a good idea to run wide open once in a while to see if your cluster has any holes??\nFor this reason, we've opted to intro an exclusion section under the image scan configuration to exclude certain images from the scans.\n\nHere is a sample configuration:\n\n```yaml\nk9s:\n  liveViewAutoRefresh: false\n  refreshRate: 2\n  ui:\n    enableMouse: false\n    headless: false\n    logoless: false\n    crumbsless: false\n    noIcons: false\n  imageScans:\n    enable: true\n    exclusions:\n      # Skip scans on these namespaces\n      namespaces:\n        - ns-1\n        - ns-2\n      # Skip scans for pods matching these labels\n      labels:\n        - app:\n          - fred\n          - blee\n          - duh\n        - env:\n          - dev\n```\n\nThis is a bit of a blur now, but I think that it! We hope you guys will dig this drop or at least the concepts as likely this is going to be `Open Season` on bugs ;(\n\n🎵 `On The second day of Christmas my true love gave to me... Eleven buggers bugging??...` 🎵\n\nLastly looks like the sponsorship stream is down to an alarming trickle so if you dig this project and find it useful be sure `to give til it hurts!`\n\n---\n\n🎅 Best wishes to you and yours for good health and happiness this holiday season!! 🎉\n\nAndJoy!\nFernand\n\n---\n\n## Resolved Issues\n\n* [#2346](https://github.com/derailed/k9s/issues/2346) k9s should not write state to config.yaml\n* [#2335](https://github.com/derailed/k9s/issues/2335) Restore 0.28 column order on pod view bug\n* [#2331](https://github.com/derailed/k9s/issues/2331) Set a shortcut key to run Vuln Scanning on a resource. Don't scan every resource at every startup.\n* [#2283](https://github.com/derailed/k9s/issues/2283) Adding auto complete in search bar\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#2357](https://github.com/derailed/k9s/pull/2357) Added ln check for snap\n* [#2350](https://github.com/derailed/k9s/pull/2350) Add symlink into snap\n* [#2348](https://github.com/derailed/k9s/pull/2348) Fix(misc plugins): split up multiline commands, use less -K everywhere\n* [#2343](https://github.com/derailed/k9s/pull/2343) Passing on the correct suggestion parameters\n* [#2341](https://github.com/derailed/k9s/pull/2340) Adding value, yaml and describe views to helm-history\n* [#2340](https://github.com/derailed/k9s/pull/2340) Add pkgx to installation section\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.30.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s-xmas.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.30.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## 🎄 Maintenance Release! 🎄\n\n🎵 `On The eleventh day of Christmas my true love gave to me... Bugs!!` 🎵\n\nGot to love the aftermath... Thank you all for pitch'in in and help flesh out bugs!! The gift that keeps on... giving?\n\n🎅 Merry Christmas to all and Best wishes for the new year!!🧑‍🎄\n\n---\n\n### Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2368](https://github.com/derailed/k9s/issues/2368) Pod CPU and MEM columns are empty in 0.30.0\n* [#2367](https://github.com/derailed/k9s/issues/2367) k9s 0.30.0 issue loading plugins\n* [#2366](https://github.com/derailed/k9s/issues/2366) List pods of deployment is now impossible\n* [#2364](https://github.com/derailed/k9s/issues/2364) k9s 0.30.0 fields and values missed in action in the \"namespace view\"\n* [#2363](https://github.com/derailed/k9s/issues/2363) Default 0.30.0 default skin on macOS is no good\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#2360](https://github.com/derailed/k9s/pull/2360) adding cancelable launch prompts to NodeShell\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.30.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s-xmas.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.30.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## 🎄 Maintenance Release! 🎄\n\n🎵 `On The eleventh day of Christmas my true love gave to me... More Bugs!!` 🎵\n\nThank you all for pitching in and help flesh out bugs!!\n\n---\n\n## [!!FEATURE NAME CHANGED!!] Vulnerability Scan Exclusions...\n\nAs it seems customary with all k9s new features, folks want to turn them off ;(\nThe `Vulscan` feature did not get out unscathed ;(\nAs it was rightfully so pointed out, you may want to opted out scans for images that you do not control.\nTho I think it might be a good idea to run wide open once in a while to see if your cluster has any holes??\nFor this reason, we've opted to intro an exclusion section under the image scan configuration to exclude certain images from the scans.\n\nHere is a sample configuration:\n\n```yaml\nk9s:\n  liveViewAutoRefresh: false\n  refreshRate: 2\n  ui:\n    enableMouse: false\n    headless: false\n    logoless: false\n    crumbsless: false\n    noIcons: false\n  imageScans:\n    enable: true\n    # MOTE!! Field Name changed!!\n    exclusions:\n      # Skip scans on these namespaces\n      namespaces:\n        - ns-1\n        - ns-2\n      # Skip scans for pods matching these labels\n      labels:\n        - app:\n          - fred\n          - blee\n          - duh\n        - env:\n          - dev\n```\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2374](https://github.com/derailed/k9s/issues/2374) The headless parameter does not function properly (v0.30.1)\n* [#2372](https://github.com/derailed/k9s/issues/2372) Unable to set default resource to load (v0.30.1)\n* [#2371](https://github.com/derailed/k9s/issues/2371) --write cli option does not work (0.30.X)\n* [#2370](https://github.com/derailed/k9s/issues/2370) Wrong list of pods on node (0.30.X)\n* [#2362](https://github.com/derailed/k9s/issues/2362) blackList: Use inclusive language alternatives\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#2375](https://github.com/derailed/k9s/pull/2375) get node filtering params from matching context values\n* [#2373](https://github.com/derailed/k9s/pull/2373) fix command line flags not working\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.30.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s-xmas.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.30.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## 🎄 Maintenance Release! 🎄\n\n🎵 `On The twelfth day of Christmas my true love gave to me... More Bugs!!` 🎵\n\nThank you all for pitching in and help flesh out issues!!\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2379](https://github.com/derailed/k9s/issues/2379) Filtering with equal sign (=) does not work in 0.30.X\n* [#2378](https://github.com/derailed/k9s/issues/2378) Logs directory not created in the k9s config/home dir 0.30.1\n* [#2377](https://github.com/derailed/k9s/issues/2377) Opening AWS EKS contexts create two directories per cluster 0.30.1\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.30.4.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s-xmas.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.30.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## 🎄 Maintenance Release! 🎄\n\nThank you all for pitching in and helping flesh out issues!!\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2391](https://github.com/derailed/k9s/issues/2391) Version 0.30.* has issues with : chars in the cluster names from AWS\n* [#2397](https://github.com/derailed/k9s/issues/2387) Error: invalid namespace xxx\n* [#2389](https://github.com/derailed/k9s/issues/2389) Mixed-case named contexts cannot be switched to from contexts view\n* [#2382](https://github.com/derailed/k9s/issues/2382) Header always shows Cluster from kubeconfig current-context\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#2390](https://github.com/derailed/k9s/pull/2390) case sensitive for specific command args and flags\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.30.5.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s-xmas.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.30.5\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## 🎄 Maintenance Release! 🎄\n\nThank you all for pitching in and helping flesh out issues!!\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2394](https://github.com/derailed/k9s/issues/2394) Allow setting custom log dir\n* [#2393](https://github.com/derailed/k9s/issues/2393) When switching contexts k9s does not switch to cluster's pod/namespaces/other k8s kinds view\n* [#2387](https://github.com/derailed/k9s/issues/2387) Invalid namespace xxx - with feelings!\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#2396](https://github.com/derailed/k9s/pull/2396) feat: allow to customize logs dir through environment variable\n* [#2395](https://github.com/derailed/k9s/pull/2395) fix: create user tmp directory before the app one\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.30.6.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s-xmas.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.30.6\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## 🎄 Maintenance Release! 🎄\n\nThank you all for pitching in and helping flesh out issues!!\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2401](https://github.com/derailed/k9s/issues/2401) Context completion broken with mixed case context names\n* [#2400](https://github.com/derailed/k9s/issues/2400) Panic on start if dns lookup fails\n* [#2387](https://github.com/derailed/k9s/issues/2387) Invalid namespace xxx - with feelings??\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.30.7.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.30.7\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\nThank you all for pitching in and helping flesh out issues!!\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2414](https://github.com/derailed/k9s/issues/2414) View pods with context filter, along with namespace filter, prompts an error if the namespace exists only in the desired context\n* [#2413](https://github.com/derailed/k9s/issues/2413) Typing apply -f in command bar causes k9s to crash\n* [#2407](https://github.com/derailed/k9s/issues/2407) Long-running background plugins block UI rendering\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#2415](https://github.com/derailed/k9s/pull/2415) Add boundary check for args parser\n* [#2411](https://github.com/derailed/k9s/pull/2411) Use dash as a standard word separator in skin names\n\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.30.8.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.30.8\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\nThank you all for pitching in and helping flesh out issues!!\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2423](https://github.com/derailed/k9s/issues/2423) CPU and MEM counters of AKS clusters show not available\n* [#2418](https://github.com/derailed/k9s/issues/2418) Boom! runtime error: invalid memory address or nil pointer dereference\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#2424](https://github.com/derailed/k9s/pull/2424) fix the check for whether the cluster supports metrics\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.31.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.31.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\n* [Border Crossing - Eek A Mouse](https://www.youtube.com/watch?v=KaAC9dBPcOM)\n* [The Weight - The Band](https://www.youtube.com/watch?v=FFqb1I-hiHE)\n* [Wonderin' - Neil Young](https://www.youtube.com/watch?v=h0PlwVPbM5k)\n* [When Your Lover Has Gone - Louis Armstrong](https://www.youtube.com/watch?v=1tdfIj0fvlA)\n\n---\n\n## A Word From Our Sponsors...\n\nTo all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!\n\n* [Jacky Nguyen](https://github.com/nktpro)\n* [Eckl, Máté](https://github.com/ecklm)\n* [Jörgen](https://github.com/wthrbtn)\n* [kmath313](https://github.com/kmath313)\n* [a-thomas-22](https://github.com/a-thomas-22)\n* [wpbeckwith](https://github.com/wpbeckwith)\n* [Dima Altukhov](https://github.com/alt-dima)\n* [Shoshin Nikita](https://github.com/ShoshinNikita)\n* [Tu Hoang](https://github.com/rebyn)\n* [Andreas Frangopoulos](https://github.com/qubeio)\n\n> Sponsorship cancellations since the last release: **7!** 🥹\n\n## Feature Release!\n\n😳 Found a few issues in the neutrino drive...\nThis is another fairly heavy drop so bracing for impact 😱\nBe sure to dial in the v0.31.0 SneakPeek video below for the gory details!\n\n😵 Hopefully we've move the needle in the right direction on this drop... 🤞\n\nThank you all for your kindness, feedback and assistance in flushing out issues!!\n\n### Hold My Hand...\n\nIn this drop, we've added schema validation to ensure various configs are setup as expected.\nK9s will now run validation checks on the following configurations:\n\n1. K9s main configuration (config.yaml)\n2. Context specific configs (clusterX/contextY/config.yaml)\n3. Skins\n4. Aliases\n5. HotKeys\n6. Plugins\n7. Views\n\nK9s behavior changed in this release if the main configuration does not match schema expectations.\nIn the past, the configuration will be validated, updated and saved should validation checks failed. Now the app will stop and report validation issues.\n\nThe schemas are set to be a bit loose for the time being. Once we/ve vetted they are cool, we could publish them out (with additional TLC!) so k9s users can leverage them in their favorite editors.\n\nIn the meantime, you'll need to keep k9s logs handy, to check for validation errors. The validation messages can be somewhat cryptic at times and so please be sure to include your debug logs and config settings when reporting issues which might be plenty ;(.\n\n### Breaking Bad!\n\nConfiguration changes:\n\n1. DRY fullScreenLogs -> fullScreens (k9s root config.yaml)\n\n   ```yaml\n   #  $XDG_CONFIG_HOME/k9s/config.yaml\n   k9s:\n     liveViewAutoRefresh: false\n     logger:\n       sinceSeconds: -1\n       fullScreen: false # => Was fullScreenLogs\n     ...\n   ```\n\n2. Views Configuration.\n   To match other configurations the root is now `views:` vs `k9s: views:`\n\n   ```yaml\n   # $XDG_CONFIG_HOME/k9s/views.yaml\n   views: # => Was k9s:\\n  views:\n    v1/pods:\n      columns:\n        - AGE\n        - NAMESPACE\n        ...\n   ```\n\n### Serenity Now!\n\n   You can now opt in/out of the `reactive ui` feature. This feature enable users to make change to some configurations and see changes reflected live in the ui. This feature is now disabled by default and one must opt-in to enable via `k9s.UI.reactive`\n   Reactive UI provides for monitoring various config files on disk and update the UI when changes to those files occur. This is handy while tuning skins, plugins, aliases, hotkeys and benchmarks parameters.\n\n   ```yaml\n   # $XDG_CONFIG_HOME/k9s/config.yaml\n   k9s:\n     liveViewAutoRefresh: false\n     UI:\n       ...\n       reactive: true # => enable/disable reactive UI\n     ...\n   ```\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2434](https://github.com/derailed/k9s/issues/2434) readOnly: true in config.yaml doesn't get overridden by readOnly: false in cluster config\n* [#2430](https://github.com/derailed/k9s/issues/2430) Referencing a namespace with the name of an alias inside an alias causes infinite loop\n* [#2428](https://github.com/derailed/k9s/issues/2428) Boom!! runtime error: invalid memory address or nil pointer dereference - v0.30.8\n* [#2421](https://github.com/derailed/k9s/issues/2421) k9s/config.yaml configuration file is overwritten on launch\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#2433](https://github.com/derailed/k9s/pull/2433) switch contexts only when needed\n* [#2429](https://github.com/derailed/k9s/pull/2429) Reference correct configuration ENV var in README\n* [#2426](https://github.com/derailed/k9s/pull/2426) Update carvel plugin kick to shift K\n* [#2420](https://github.com/derailed/k9s/pull/2420) supports referencing envs in hotkeys\n* [#2419](https://github.com/derailed/k9s/pull/2419) fix typo\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.31.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.31.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\n* [Border Crossing - Eek A Mouse](https://www.youtube.com/watch?v=KaAC9dBPcOM)\n* [The Weight - The Band](https://www.youtube.com/watch?v=FFqb1I-hiHE)\n* [Wonderin' - Neil Young](https://www.youtube.com/watch?v=h0PlwVPbM5k)\n* [When Your Lover Has Gone - Louis Armstrong](https://www.youtube.com/watch?v=1tdfIj0fvlA)\n\n---\n\n## A Word From Our Sponsors...\n\nTo all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!\n\n* [Jacky Nguyen](https://github.com/nktpro)\n* [Eckl, Máté](https://github.com/ecklm)\n* [Jörgen](https://github.com/wthrbtn)\n* [kmath313](https://github.com/kmath313)\n* [a-thomas-22](https://github.com/a-thomas-22)\n* [wpbeckwith](https://github.com/wpbeckwith)\n* [Dima Altukhov](https://github.com/alt-dima)\n* [Shoshin Nikita](https://github.com/ShoshinNikita)\n* [Tu Hoang](https://github.com/rebyn)\n* [Andreas Frangopoulos](https://github.com/qubeio)\n\n> Sponsorship cancellations since the last release: **7!** 🥹\n\n## Feature Release!\n\n😳 Found a few issues in the neutrino drive...\nThis is another fairly heavy drop so bracing for impact 😱\nBe sure to dial in the v0.31.0 SneakPeek video below for the gory details!\n\n😵 Hopefully we've move the needle in the right direction on this drop... 🤞\n\nThank you all for your kindness, feedback and assistance in flushing out issues!!\n\n> ☢️ Repeating v0.31.0 release notes here as we tweaked the initial drop ☢️\n\n### Hold My Hand...\n\nIn this drop, we've added schema validation to ensure various configs are setup as expected.\nK9s will now run validation checks on the following configurations:\n\n1. K9s main configuration (config.yaml)\n2. Context specific configs (clusterX/contextY/config.yaml)\n3. Skins\n4. Aliases\n5. HotKeys\n6. Plugins\n7. Views\n\nK9s behavior changed in this release if the main configuration does not match schema expectations.\nIn the past, the configuration will be validated, updated and saved should validation checks failed. Now the app will stop and report validation issues.\n\nThe schemas are set to be a bit loose for the time being. Once we/ve vetted they are cool, we could publish them out (with additional TLC!) so k9s users can leverage them in their favorite editors.\n\nIn the meantime, you'll need to keep k9s logs handy, to check for validation errors. The validation messages can be somewhat cryptic at times and so please be sure to include your debug logs and config settings when reporting issues which might be plenty ;(.\n\n### Breaking Bad!\n\nWith this release, k9s may not start correctly if the config.yaml configurations are incorrect!\n\nConfiguration changes:\n\n1. DRY fullScreenLogs -> fullScreens (k9s root config.yaml)\n\n   ```yaml\n   #  $XDG_CONFIG_HOME/k9s/config.yaml\n   k9s:\n     liveViewAutoRefresh: false\n     logger:\n       sinceSeconds: -1\n       fullScreen: false # => Was fullScreenLogs\n     ...\n   ```\n\n2. Views Configuration.\n   To match other configurations the root is now `views:` vs `k9s: views:`\n\n   ```yaml\n   # $XDG_CONFIG_HOME/k9s/views.yaml\n   views: # => Was k9s:\\n  views:\n    v1/pods:\n      columns:\n        - AGE\n        - NAMESPACE\n        ...\n   ```\n\n### Serenity Now!\n\n   You can now opt in/out of the `reactive ui` feature. This feature enable users to make change to some configurations and see changes reflected live in the ui. This feature is now disabled by default and one must opt-in to enable via `k9s.UI.reactive`\n   Reactive UI provides for monitoring various config files on disk and update the UI when changes to those files occur. This is handy while tuning skins, plugins, aliases, hotkeys and benchmarks parameters.\n\n   ```yaml\n   # $XDG_CONFIG_HOME/k9s/config.yaml\n   k9s:\n     liveViewAutoRefresh: false\n     UI:\n       ...\n       reactive: true # => enable/disable reactive UI\n     ...\n   ```\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2434](https://github.com/derailed/k9s/issues/2434) readOnly: true in config.yaml doesn't get overridden by readOnly: false in cluster config\n* [#2430](https://github.com/derailed/k9s/issues/2430) Referencing a namespace with the name of an alias inside an alias causes infinite loop\n* [#2428](https://github.com/derailed/k9s/issues/2428) Boom!! runtime error: invalid memory address or nil pointer dereference - v0.30.8\n* [#2421](https://github.com/derailed/k9s/issues/2421) k9s/config.yaml configuration file is overwritten on launch\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#2433](https://github.com/derailed/k9s/pull/2433) switch contexts only when needed\n* [#2429](https://github.com/derailed/k9s/pull/2429) Reference correct configuration ENV var in README\n* [#2426](https://github.com/derailed/k9s/pull/2426) Update carvel plugin kick to shift K\n* [#2420](https://github.com/derailed/k9s/pull/2420) supports referencing envs in hotkeys\n* [#2419](https://github.com/derailed/k9s/pull/2419) fix typo\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.31.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.31.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\nYikes! The aftermath...\n\nThank you all for pitching in and helping flesh out issues!!\n\nPlease make sure to add gory details to issues ie relevant configs, debug logs, etc...\n\nComments like: `same here!` doesn't really help us zero in. Everyone has slightly different settings/platforms so every little bits of info helps with the resolves.\nThank you!!\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2449](https://github.com/derailed/k9s/issues/2449) [Bug]: views.yaml columns not respected on startup\n* [#2448](https://github.com/derailed/k9s/issues/2448) Missing '.thresholds' in config.yaml result in 'assignment to entry in nil map'\n* [#2446](https://github.com/derailed/k9s/issues/2446) Context Switch unreliable/not working\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.31.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.31.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\nThe aftermath...\n\nThank you all for pitching in and helping flesh out issues!!\n\nPlease make sure to add gory details to issues ie relevant configs, debug logs, etc...\n\nComments like: `same here!` doesn't really help us zero in. Everyone has slightly different settings/platforms so every little bits of info helps with the resolves.\nThank you!!\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2459](https://github.com/derailed/k9s/issues/2459) No permission to see deployments/statefulsets even though I have them\n* [#2458](https://github.com/derailed/k9s/issues/2458) panic on run without current context\n* [#2454](https://github.com/derailed/k9s/issues/2454) Invoking K9s ends in panic question\n* [#2435](https://github.com/derailed/k9s/issues/2435) \"yaml: line 15: could not find expected ':'\" error bug question (May be??)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.31.4.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.31.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\nMore aftermath...\n\nThank you all for pitching in and helping flesh out issues!!\n\nPlease make sure to add gory details to issues ie relevant configs, debug logs, etc...\n\nComments like: `same here!` or `me to!` doesn't really help us zero in.\nEveryone has slightly different settings/platforms so every little bits of info helps with the resolves.\nThank you!!\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2463](https://github.com/derailed/k9s/issues/2463) v0.31.3 (Linux_amd64) gives runtime error on startup\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)\n"
  },
  {
    "path": "change_logs/release_v0.31.5.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.31.5\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n😱 More aftermath... 😱\n\nThank you all for pitching in and helping flesh out issues!!\n\nPlease make sure to add gory details to issues ie relevant configs, debug logs, etc...\n\nComments like: `same here!` or `me to!` doesn't really cut it for us to zero in ;(\nEveryone has slightly different settings/platforms so every little bits of info helps with the resolves even if seemingly irrelevant.\nThank you!!\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2466](https://github.com/derailed/k9s/issues/2466) Panic: index out of range [0] with length 0\n* [#2465](https://github.com/derailed/k9s/issues/2465) v0.31.4 - panic; no client connection detected - with feelings!!\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.31.6.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.31.6\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n😱 More aftermath... 😱\n\nThank you all for pitching in and helping flesh out issues!!\n\nPlease make sure to add gory details to issues ie relevant configs, debug logs, etc...\n\nComments like: `same here!` or `me to!` doesn't really cut it for us to zero in ;(\nEveryone has slightly different settings/platforms so every little bits of info helps with the resolves even if seemingly irrelevant.\n\n---\n\n## NOTE\n\nIn this drop, we've made k9s a bit more resilient (hopefully!) to configuration issues and in most cases k9s will come up but may exhibit `limp mode` behaviors.\nPlease double check your k9s logs if things don't work as expected and file an issue with the `gory` details!\n\n☢️ This drop may cause `some disturbance in the farce!` ☢️\n\nPlease proceed with caution with this one as we did our best to attempt to address potential context config file corruption by eliminating race conditions.\nIt's late and I am operating on minimal sleep so I may have hosed some behaviors 🫣\nIf you experience k9s locking up or misbehaving, as per the above👆 you know what to do now and as customary\nwe will do our best to address them quickly to get you back up and running!\n\nThank you for your support, kindness and patience!\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2476](https://github.com/derailed/k9s/issues/2476) Pods are not displayed for the selected namespace. Hopefully!\n* [#2471](https://github.com/derailed/k9s/issues/2471) Shell autocomplete functions do not work correctly\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#2480](https://github.com/derailed/k9s/pull/2480) Adding system arch to nodes view\n* [#2477](https://github.com/derailed/k9s/pull/2477) Shell autocomplete for k8s flags\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.31.7.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.31.7\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n😱 More aftermath... 😱\n\nThank you all for pitching in and helping flesh out issues!!\n\nPlease make sure to add gory details to issues ie relevant configs, debug logs, etc...\n\nComments like: `same here!` or `me to!` doesn't really cut it for us to zero in ;(\nEveryone has slightly different settings/platforms so every little bits of info helps with the resolves even if seemingly irrelevant.\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2488](https://github.com/derailed/k9s/issues/2488) linux_amd64 \"--kubeconfig\" not working on v0.31.6\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.31.8.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.31.8\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\nThank you all for pitching in and helping flesh out issues!!\n\nPlease make sure to add gory details to issues ie relevant configs, debug logs, etc...\n\nComments like: `same here!` or `me to!` doesn't really cut it for us to zero in ;(\nEveryone has slightly different settings/platforms so every little bits of info helps with the resolves even if seemingly irrelevant.\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\nGoing back to the classics...\n\n* [Ambulance Blues - Neil Young](https://www.youtube.com/watch?v=bCQisTEdBwY)\n* [Christopher Columbus - Burning Spear](https://www.youtube.com/watch?v=5qbMKTY_Cr0)\n* [Feelin' the Same - Clinton Fearon](https://www.youtube.com/watch?v=aRPF2Yta_cs)\n\n---\n\n## A Word From Our Sponsors...\n\nTo all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!\n\n* [Andreas Frangopoulos](https://github.com/qubeio)\n* [Tu Hoang](https://github.com/rebyn)\n* [Shoshin Nikita](https://github.com/ShoshinNikita)\n* [Dima Altukhov](https://github.com/alt-dima)\n* [wpbeckwith](https://github.com/wpbeckwith)\n* [a-thomas-22](https://github.com/a-thomas-22)\n* [kmath313](https://github.com/kmath313)\n* [Jörgen](https://github.com/wthrbtn)\n* [Eckl, Máté](https://github.com/ecklm)\n* [Jacky Nguyen](https://github.com/nktpro)\n* [Chris Bradley](https://github.com/chrisbradleydev)\n* [Vytautas Kubilius](https://github.com/vytautaskubilius)\n* [Patrick Christensen](https://github.com/BuriedStPatrick)\n* [Ollie Lowson](https://github.com/ollielowson-wcbs)\n* [Mike Macaulay](https://github.com/mmacaula)\n* [David Birks](https://github.com/dbirks)\n* [James Hounshell](https://github.com/jameshounshell)\n* [elapse2039](https://github.com/elapse2039)\n* [Vinicius Xavier](https://github.com/vinixaavier)\n* [Phuc Phung](https://github.com/Foxhound401)\n* [ollielowson](https://github.com/ollielowson)\n\n> Sponsorship cancellations since the last release: **4!** 🥹\n\n---\n\n## Resolved Issues\n\n* [#2527](https://github.com/derailed/k9s/issues/2527) Multiple k9s panels open in parallel for the same cluster breaks config.yaml\n* [#2520](https://github.com/derailed/k9s/issues/2520) pods with init container with restartPolicy: Always stay in Init status\n* [#2501](https://github.com/derailed/k9s/issues/2501) Cannot add plugins to helm scope bug\n* [#2492](https://github.com/derailed/k9s/issues/2492) API Resources \"carry over\" between contexts, causing errors if they share shortnames\n* [#1158](https://github.com/derailed/k9s/issues/1158) Removing a helm release incorrectly determines the namespace of resources\n* [#1033](https://github.com/derailed/k9s/issues/1033) Helm delete deletes only the helm entry but not the deployment\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#2509](https://github.com/derailed/k9s/pull/2509) Fix Toggle Faults filtering\n* [#2511](https://github.com/derailed/k9s/pull/2511) adding the f command to pf extender view\n* [#2518](https://github.com/derailed/k9s/pull/2518) Added defaultsToFullScreen flag for Live/Details view,logs\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.31.9.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.31.9\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n```text\nS          .-'-.\n o     __| F    `\\\n  S   `-,-`--._   `\\\n []  .->'  X     `|-'\n  `=/ (__/_       /\n    \\_,    `    _)\n       `----;  |\n```\n\n⛔️ WE HAVE A PIPER DOWN! I REPEAT PIPER IS DOWN!! ⛔️\n\nPopeye is undergoing heavy surgery at the moment so I had to break the bridge.\nIf you dig Popeye please run the binary separately for the time being.\nI'll post another message here once the spinach formula upgrade is successful!\n\nAlso please make sure to add the gory details to issues ie relevant configs, debug logs, etc...\nComments like: `same here!` or `me to!` doesn't really cut it for us to zero in ;(\n\nEveryone has slightly different settings/platforms so every little bits of info helps with the resolves even if seemingly irrelevant.\n\nThank you all for pitching in and helping flesh out issues!!\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\nUshered or Taylored out?\n\n* [Rough God Goes Riding - Van Morrison](https://www.youtube.com/watch?v=-kGrwRlJxcM)\n* [Walk On - John Hiatt](https://www.youtube.com/watch?v=YVdMyeTQCkw)\n* [On The Beach - Neil Young](https://www.youtube.com/watch?v=KBVde75e4sU)\n\n---\n\n## A Word From Our Sponsors...\n\nTo all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!\n\n* [Francis Lalonde](https://github.com/f-lalonde)\n* [e-conomic a/s](https://github.com/e-conomic)\n\n> Sponsorship cancellations since the last release: **2!** 🥹\n\n---\n\n## Resolved Issues\n\n* [#2540](https://github.com/derailed/k9s/issues/2540) Option --write not functional\n* [#2538](https://github.com/derailed/k9s/issues/2538) Opening screen dumps (sd) in K9s results in Failed to launch editor error message\n* [#2536](https://github.com/derailed/k9s/issues/2536) Recent namespaces are lost when changing context\n* [#2535](https://github.com/derailed/k9s/issues/2535) Namespaced configmap edit fails for user with RoleBinding to a role that allows it\n* [#2532](https://github.com/derailed/k9s/issues/2532) Sporadic crashes (Maybe??)\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#2541](https://github.com/derailed/k9s/pull/2541) Add Rose Pine moon and dawn variants to skins\n* [#2531](https://github.com/derailed/k9s/pull/2531) fix the --write flag\n* [#2516](https://github.com/derailed/k9s/pull/2516) Added defaultsToFullScreen flag for Live/Details view,logs\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.32.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.32.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\nA lot of refactors, perf improvements (crossing fingers+toes!) and general spring cleaning items in this release.\nThus I expect a bit of `disturbance in the farce` given the major code churns, so please beware!\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## A Word From Our Sponsors...\n\nTo all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!\n\n* [Justin Reid](https://github.com/jmreid)\n* [Danni](https://github.com/danninov)\n* [Robert Krahn](https://github.com/rksm)\n* [Hao Ke](https://github.com/kehao95)\n* [PH](https://github.com/raphael-com-ph)\n\n> Sponsorship cancellations since the last release: **9!!** 🥹\n\n---\n\n## Resolved Issues\n\n* [#2569](https://github.com/derailed/k9s/issues/2569) k9s panics on start if the main config file (config.yml) is owned by root\n* [#2568](https://github.com/derailed/k9s/issues/2568) kube context in running k9s is no longer sticky, during kubectx context switch\n* [#2560](https://github.com/derailed/k9s/issues/2560) Namespace/Settings keeps resetting\n* [#2557](https://github.com/derailed/k9s/issues/2557) [Feature]: Sort CRDs by their group\n* [#1462](https://github.com/derailed/k9s/issues/1462) k9s running very slowly when opening namespace with 13k pods (maybe??)\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#2564](https://github.com/derailed/k9s/pull/2564) Add everforest skins\n* [#2558](https://github.com/derailed/k9s/pull/2558) feat: sort by role in node list view\n* [#2554](https://github.com/derailed/k9s/pull/2554) Added context to the debug command for debug-container plugin\n* [#2554](https://github.com/derailed/k9s/pull/2554) Correctly respect the KUBECACHEDIR env var\n* [#2546](https://github.com/derailed/k9s/pull/2546) Use configured log fgColor to print log markers\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.32.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.32.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\nThe aftermath ;(\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2584](https://github.com/derailed/k9s/issues/2584) Transfer of file doesn't detect corruption\n* [#2579](https://github.com/derailed/k9s/issues/2579) Default sorting behavior changed to descending sort bug\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#2586](https://github.com/derailed/k9s/pull/2586) Properly initialize key actions in picker\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.32.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.32.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\nMo aftermath ;(\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2582](https://github.com/derailed/k9s/issues/2582) Slowness due to client-side throttling in v0.32.0  (Maybe??)\n* [#2593](https://github.com/derailed/k9s/issues/2593) Popeye not working in 0.32.X\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.32.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.32.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\nLook like v0.32.2 drop release bins are toast. So m'o aftermath ;(\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2584](https://github.com/derailed/k9s/issues/2584) Transfer of file doesn't detect corruption (with feelings!)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.32.4.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.32.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\nThinking of all you at KubeCon Paris!!\nMay I suggest a nice glass of `cold Merlote` or other fine grape juices from my country?\n\n* [Le Gorille - George Brassens](https://www.youtube.com/watch?v=KVfwvk_yVyA)\n* [Les Funerailles D'antan (Love this guy!) - George Brassens](https://www.youtube.com/watch?v=bwb5k4k2EMc)\n* [Poinconneur Des Lilas - Serge Gainsbourg](https://www.youtube.com/watch?v=eWkWCFzkOvU)\n* [Mon Legionaire (Yup! same guy??) - Serge Gainsbourg](https://www.youtube.com/watch?v=gl8gopryqWI)\n* [Les Cornichons - Nino Ferrer](https://www.youtube.com/watch?v=N7JSW4NhM8I)\n* [Paris s'eveille - Jacques Dutronc](https://www.youtube.com/watch?v=3WcCg6rm3uM)\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2608](https://github.com/derailed/k9s/issues/2608) Make the sanitize feature easier to use\n* [#2605](https://github.com/derailed/k9s/issues/2605) Built-in shortcuts being overridden by plugins result in excessive logging\n* [#2604](https://github.com/derailed/k9s/issues/2604) Ability to mark a plugin as Dangerous/destructive\n* [#2592](https://github.com/derailed/k9s/issues/2592) \"list access denied\" when switching contexts within k9s since 0.32.0\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#2621](https://github.com/derailed/k9s/pull/2621) Fix snap build\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.32.5.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.32.5\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2734](https://github.com/derailed/k9s/issues/2734) Incorrect pod containers displayed when using custom resource columns\n* [#2733](https://github.com/derailed/k9s/issues/2733) Toggle Wide and Toggle Faults broken for PDB view\n* [#2656](https://github.com/derailed/k9s/issues/2656) nil pointer dereference when switching contexts\n* [#2617](https://github.com/derailed/k9s/issues/2617) Plugin command execution output\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#2736](https://github.com/derailed/k9s/pull/2736) fix view sorting being reset\n* [#2732](https://github.com/derailed/k9s/pull/2732) use policy/v1 instead of policy/v1beta1\n* [#2728](https://github.com/derailed/k9s/pull/2728) feat: add pool col to node view\n* [#2718](https://github.com/derailed/k9s/pull/2718) fix: jump to namespaceless owner reference\n* [#2711](https://github.com/derailed/k9s/pull/2711) Add plugins for argo-rollouts\n* [#2700](https://github.com/derailed/k9s/pull/2700) feat: allow jumping to the owner of the resource\n* [#2699](https://github.com/derailed/k9s/pull/2699) Added cert-manager and openssl plugins\n* [#2711](https://github.com/derailed/k9s/pull/2711) Add plugins for argo-rollouts\n* [#2698](https://github.com/derailed/k9s/pull/2698) fix: job color based on failures (#2686)\n* [#2685](https://github.com/derailed/k9s/pull/2685) feat: support cluster and cmp view\n* [#2678](https://github.com/derailed/k9s/pull/2678) fix: do not hard-code path to kubectl in jq plugin\n* [#2676](https://github.com/derailed/k9s/pull/2676) Add kanagawa skin\n* [#2666](https://github.com/derailed/k9s/pull/2666) save config when closing k9s with ctrl-c\n* [#2644](https://github.com/derailed/k9s/pull/2644) Allow overwriting plugin output with command's stdout\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.32.6.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.32.6\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2947](https://github.com/derailed/k9s/issues/2947) CTRL+Z causes k9s to crash\n* [#2938](https://github.com/derailed/k9s/issues/2938) Critical Vulnerability CVE-2024-41110 in v26.0.1 of docker included in k9s\n* [#2929](https://github.com/derailed/k9s/issues/2929) conflicting plugins shortcuts\n* [#2896](https://github.com/derailed/k9s/issues/2896) Add a plugin to disable/enable a keda ScaledObject\n* [#2811](https://github.com/derailed/k9s/issues/2811) Dockerfile build step fails due to misaligned Go versions (1.21.5 vs 1.22.0)\n* [#2767](https://github.com/derailed/k9s/issues/2767) Manually triggered jobs don't get automatically cleaned up\n* [#2761](https://github.com/derailed/k9s/issues/2761) Enable \"jump to owner\" for more kinds\n* [#2754](https://github.com/derailed/k9s/issues/2754) Plugins not loaded/shown in UI\n* [#2747](https://github.com/derailed/k9s/issues/2747) Combining context and namespace switching only works sporadically (e.g. \":pod foo-ns @ctx-dev\")\n* [#2746](https://github.com/derailed/k9s/issues/2746) k9s does not display \"[::]\" string in its logs\n* [#2738](https://github.com/derailed/k9s/issues/2738) \"Faults\" view should show all Terminating pods\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#2937](https://github.com/derailed/k9s/pull/2937) Adding Argo Rollouts plugin version for PowerShell\n* [#2935](https://github.com/derailed/k9s/pull/2935) fix: show all terminating pods in Faults view (#2738)\n* [#2933](https://github.com/derailed/k9s/pull/2933) chore: broken url in build-status tag in the readme.md\n* [#2932](https://github.com/derailed/k9s/pull/2932) fix: add kubeconfig if k9s is launched with --kubeconfig\n* [#2930](https://github.com/derailed/k9s/pull/2930) fixed conflicting plugin shortcuts, and added 2 new plugins\n* [#2927](https://github.com/derailed/k9s/pull/2927) Fix \"Mark Range\": reduce maximum namespaces in favorites, fix shadowing of ctrl+space\n* [#2926](https://github.com/derailed/k9s/pull/2926) chore(plugins,remove-finalizers): make sure the resources api group is respected\n* [#2921](https://github.com/derailed/k9s/pull/2921) feat: Add plugins for kubectl node-shell\n* [#2920](https://github.com/derailed/k9s/pull/2920) eat: added StartupProbes status (S) to the PROBES column in the container render\n* [#2914](https://github.com/derailed/k9s/pull/2914) Adding eks-node-viewer plugin\n* [#2898](https://github.com/derailed/k9s/pull/2898) Add argocd plugin to community plugins\n* [#2896](https://github.com/derailed/k9s/pull/2896) feat(2896): Add toggle keda plugin\n* [#2890](https://github.com/derailed/k9s/pull/2890) Update README.md\n* [#2881](https://github.com/derailed/k9s/pull/2881) Fix Mark-Range command: ensure that NS Favorite doesn't exceed the limit\n* [#2861](https://github.com/derailed/k9s/pull/2861) chore: fix function name\n* [#2856](https://github.com/derailed/k9s/pull/2856) fix internal/render/hpa.go merge issue\n* [#2848](https://github.com/derailed/k9s/pull/2848) Include sidecar containers requests and limits\n* [#2844](https://github.com/derailed/k9s/pull/2844) Update README GO Version Required\n* [#2830](https://github.com/derailed/k9s/pull/2830) update tview to fix log escaping problem completely\n* [#2822](https://github.com/derailed/k9s/pull/2822) Adding HolmesGPT plugin\n* [#2821](https://github.com/derailed/k9s/pull/2821) Add a spark-operator plugin\n* [#2817](https://github.com/derailed/k9s/pull/2817) Add comment about Escape keybinding\n* [#2812](https://github.com/derailed/k9s/pull/2812) fix: align build image Go version with go.mod\n* [#2795](https://github.com/derailed/k9s/pull/2795) add new plugin current-ctx-terminal\n* [#2791](https://github.com/derailed/k9s/pull/2791) Add leading space to Kubernetes context suggestions\n* [#2789](https://github.com/derailed/k9s/pull/2789) Create kubectl-get-in-shell.yaml\n* [#2788](https://github.com/derailed/k9s/pull/2788) Update README.md plugin format\n* [#2787](https://github.com/derailed/k9s/pull/2787) Update helm-purge.yaml\n* [#2786](https://github.com/derailed/k9s/pull/2786) Update README.md with plugin dangerous field\n* [#2780](https://github.com/derailed/k9s/pull/2780) install copyright file into correct location\n* [#2775](https://github.com/derailed/k9s/pull/2775) fix freebsd build failure\n* [#2780](https://github.com/derailed/k9s/pull/2780) install copyright file into correct location\n* [#2772](https://github.com/derailed/k9s/pull/2772) proper handle OwnerReference for manually created job\n* [#2771](https://github.com/derailed/k9s/pull/2771) feat: add duplik8s plugin\n* [#2770](https://github.com/derailed/k9s/pull/2770) feat: allow plugins block in plugin files\n* [#2765](https://github.com/derailed/k9s/pull/2765) fix: Shellin -> ShellIn\n* [#2763](https://github.com/derailed/k9s/pull/2763) enable \"jump to owner\" for more kinds\n* [#2755](https://github.com/derailed/k9s/pull/2755) Loki plugin\n* [#2751](https://github.com/derailed/k9s/pull/2751) container logs should be escaped when printed\n* [#2750](https://github.com/derailed/k9s/pull/2750) fix: should switching ctx before ns\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.32.7.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.32.7\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#2970](https://github.com/derailed/k9s/issues/2970) Ctrl-z on events view causes runtime error in v0.32.6\n* [#2969](https://github.com/derailed/k9s/issues/2969) When using impersonation user information and permissions not preserved when switching context\n* [#2966](https://github.com/derailed/k9s/issues/2966) Go to the Contexts page and filter, contexts that are matched will be filtered ou\n* [#2962](https://github.com/derailed/k9s/issues/2962) Small colour/filtering related bug\n* [#2961](https://github.com/derailed/k9s/issues/2961) Drain node with the -disable-eviction\n* [#2958](https://github.com/derailed/k9s/issues/2958) Restart count in container view associated with the wrong container\n* [#2945](https://github.com/derailed/k9s/issues/2945) Could we add ServiceAccount Column in v1/POD view\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#2968](https://github.com/derailed/k9s/pull/2968) Update go version to 1.23.X in README\n* [#2964](https://github.com/derailed/k9s/pull/2964) feat(dao,used-by-cmd): check imagePullSecrets as well\n* [#2960](https://github.com/derailed/k9s/pull/2960) Put log levels in order in cmd help\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.40.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.40.0\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\n* [Glory Box - Portishead](https://www.youtube.com/watch?v=4qQyUi4zfDs)\n* [Hit Me With Your Rhythm Stick - Ian Dury And The BlockHeads](https://www.youtube.com/watch?v=0WGVgfjnLqc)\n* [Cupidon s'en fout! - George Brassens](https://www.youtube.com/watch?v=a-RlZLfIeKM)\n* [Shipbuilding - Elvis Costello](https://www.youtube.com/watch?v=dVhjRqBM5uw)\n* [Low Sun - Hermanos Gutierrez](https://www.youtube.com/watch?v=ubaJbw7hkeQ)\n\n---\n\n## A Word From Our Sponsors...\n\nTo all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!\n\n* [Panfactum](https://github.com/Panfactum)\n* [Bastian Pätzold](https://github.com/bastianpaetzold)\n* [Mikita Vazhnik](https://github.com/Vazhnik)\n* [Jacob Salway](https://github.com/jacobsalway)\n* [Eckard Mühlich](https://github.com/eckardnet)\n* [Luke](https://github.com/lukepatrick)\n* [tomasbanet](https://github.com/tomasbanet)\n* [Robin Opletal](https://github.com/fourstepper)\n* [Euroblaze](https://github.com/euroblaze)\n* [Jack Daniels](https://github.com/dkr91)\n* [decafcode](https://github.com/decafcode)\n* [Guillaume Copin](https://github.com/GuillaumeCo)\n* [Lokalise](https://github.com/lokalise)\n* [Gustavo Bini](https://github.com/gustavobini)\n* [JMSwag](https://github.com/JMSwag)\n* [Daniel Gospodinow](https://github.com/danielgospodinow)\n* [Klaviyo](https://github.com/klaviyo)\n* [Paul Farver](https://github.com/PaulFarver)\n\n> Sponsorship cancellations since the last release: **12!** 🥹\n\n## 🎉 Feature Release code name: Colon Blow! 🎈\n\nWe are pretty stocked about this drop (hopefully...) as we've fully enabled custom columns support in K9s!\nHistorically, one could customize the view for a given resource by adding a definition in `views.yaml`.\nFrom there one could change sort order and re-arrange the standard column layout.\nSeveral folks voiced the need to add a column for a given label/annotation or any other fields available on a resource.\nTo date, this wasn't possible 😳\n\nSo... without further ado, let see what we can now do with `Custom Views` ding dang deal!\nIt all starts with a few new directives available in `views.yaml`\n\n### A Refresher...\n\nCustomize a pod view and ensure age, ns and name appear first and sort by age descending.\n\n> NOTE! You no longer need to list out all columns.\n> The remaining columns will be automatically filled from the standard columns.\n\n```yaml\n# Usual biz...\nviews:\n  v1/pods:                         # specify the gvr you want to customize aka group/version/resource\n    sortColumn: AGE:desc           # set the default ordering to ascending (asc) or descending (desc)\n    columns:                       # tell the view which columns to display and in which order\n      - AGE                        # ensure age, ns and name are the first 3 cols and backfill the rest\n      - NAMESPACE\n      - NAME\n      - READY|H                    # => NEW! Do not display the READY column\n      - NODE|W                     # => NEW! Show node column only on wide\n      - IP|WR                      # => NEW! Pull the ip column and right align it in wide mode only\n```\n\n## Colon Blow!\n\nSay your pods comes standard with a label `blee` and you want to show it while in pod view.\n\n```yaml\n# Pull labels/annotations\nviews:\n  v3/freds:\n    sortColumn: NAMESPACE:dsc\n    columns:\n      - NAMESPACE\n      - NAME\n      - BLEE:.metadata.labels.blee                        # => NEW! Pull values from a label or an annotation using json parser\n                                                          # expression similar mechanic as kubectl -o custom-columns\n      - ZORG:.spec.zips[?(@.type == 'zorg')].ip|WR        # => NEW! Same deal with a json exp + but align right and show wide only\n```\n\n## TLDR...\n\nAs you can see the CustomView feature adds a few new semantics on this drop.\n\nYou can now use the following shape for columns definition `COL_NAME<:json_parse_expression><|column attributes>`\n\nThe `:json_parse_expression` is optional.\n\nThe column attributes are as follows:\n\n* `T` -> time column indicator\n* `N` -> number column indicator\n* `W` -> turns on wide column aka only shows while in wide mode. Defaults to the standard resource definition when present.\n* `H` -> Hides the column\n* `L` -> Left align (default)\n* `R` -> Right align\n\nWhen certain columns are not present in the custom view, K9s will pull the standard column definition and merge the columns.\nThis allows user to specify and order which columns they want to see first without having to define every single columns from the default resource representation. If you do not wish to see all these columns you can add them to your custom view definition and either specify `|W` or `|H` to `wide` it or `hide` it.\n\n> 📢 Still work in progress so your mileage may vary!\n> This feature will likely need additional TLC.\n> Your feedback on this will be much appreciated and we will iterate as usual to ensure it vorks as prescribed... 🙀\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.40.0 Colon Blow Sneak peek](https://youtu.be/iy6RDozAM4A)\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#3064](https://github.com/derailed/k9s/issues/3064) Question: brew formula k9s vs derailed/k9s/k9s\n* [#3061](https://github.com/derailed/k9s/issues/3061) k9s not opening active namespace or namespace specified via -n\n* [#3044](https://github.com/derailed/k9s/issues/3044) CRDs are loaded incorrectly into metadata registry, cause sporadic \"Jump Owner\" issues\n* [#2995](https://github.com/derailed/k9s/issues/2995) Latest image on quay.io contains \"failed\" kubectl binary\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#3065](https://github.com/derailed/k9s/pull/3065) Fixed trimming of favorite namespaces in Config\n* [#3063](https://github.com/derailed/k9s/pull/3063) Updating CVE dependencies\n* [#3062](https://github.com/derailed/k9s/pull/3062) feat: use kubectl events for plugin watch-events\n* [#3060](https://github.com/derailed/k9s/pull/3060) Rename \"delete local data\" checkbox description in drain dialog\n* [#3046](https://github.com/derailed/k9s/pull/3046) Strict unmarshal for plugin files\n* [#3045](https://github.com/derailed/k9s/pull/3045) fix: CRD loading: trim group suffix from CRD name\n* [#3043](https://github.com/derailed/k9s/pull/3043) Fix K9S_EDITOR\n* [#3041](https://github.com/derailed/k9s/pull/3041) Fix Flux trace plugin command\n* [#3038](https://github.com/derailed/k9s/pull/2038) fix check e != nil but return a nil value error err\n* [#3026](https://github.com/derailed/k9s/pull/3026) Fix typos\n* [#3018](https://github.com/derailed/k9s/pull/3018) fix: coloring of rose-pine for values of log options\n* [#3017](https://github.com/derailed/k9s/pull/3017) feat: add helm diff plugin\n* [#3009](https://github.com/derailed/k9s/pull/3009) fix(argo-rollouts plugin): resolve improper piping in watch command\n* [#2996](https://github.com/derailed/k9s/pull/2996) Bump version of netshoot image in debug-container plugin\n* [#2994](https://github.com/derailed/k9s/pull/2994) fix kubectl url and fail build on download errors\n* [#2986](https://github.com/derailed/k9s/pull/2986) plugin/trace-dns: Trace DNS requests using Inspektor Gadget\n* [#2985](https://github.com/derailed/k9s/pull/2985) feat(plugins/crossplane): change to crossplane cli & add crossplane-watch\n* [#2986](https://github.com/derailed/k9s/pull/2986) plugin/trace-dns: Trace DNS requests using Inspektor Gadget\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.40.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.40.1\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n😳 Aye! Buzz kill on the 0.40.0 aftermath... 🙀 👻\n\nLikely additional `disturbance in the farce` might be observed.\nThank you all for giving v0.40.0 a rinse and reporting back!! 😍\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.40.0 -Column Blow- Sneak peek](https://youtu.be/iy6RDozAM4A)\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#3113](https://github.com/derailed/k9s/issues/3113) 0.40.0 can't retain temporary view sort\n* [#3111](https://github.com/derailed/k9s/issues/3111) k9s can't describe or print YAML for HPAs in all namespaces view\n* [#2966](https://github.com/derailed/k9s/issues/2966) Go to the Contexts page and filter, contexts that are matched will be filtered ou\n* [#2962](https://github.com/derailed/k9s/issues/2962) Small colour/filtering related bug\n* [#2961](https://github.com/derailed/k9s/issues/2961) Drain node with the -disable-eviction\n* [#2958](https://github.com/derailed/k9s/issues/2958) Restart count in container view associated with the wrong container\n* [#2945](https://github.com/derailed/k9s/issues/2945) Could we add ServiceAccount Column in v1/POD view\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#3094](https://github.com/derailed/k9s/pull/3094) Log in as root to the node.\n* [#3033](https://github.com/derailed/k9s/pull/3033) Skip cache invalidation on failed connection\n* [#2965](https://github.com/derailed/k9s/pull/2965) Make menu foreground style configurable through skins\n* [#2952](https://github.com/derailed/k9s/pull/2952) A modest attempt to improve the logo aesthetics\n* [#2833](https://github.com/derailed/k9s/pull/2833) allow scaling custom resource\n* [#2799](https://github.com/derailed/k9s/pull/2799) feat(app): add history navigation with [ and ], most recent command with -\n* [#2719](https://github.com/derailed/k9s/pull/2719) fix: stop table header cells from being selectable\n* [#2865](https://github.com/derailed/k9s/pull/2865) Feature/DisableAutoscroll\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.40.10.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.40.10\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\nSounds like I did hose plugins after all... With feelings!\n\n* Refactored plugins implementation, hopefully we didn't hose them 😳\n* Updated plugins docs\n* Apparently when it comes to icons, I've chosen... poorly 🙀\n  Updated `write` icon 🔓->✍️, hopefully for the better 👀??\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.40.0 -Column Blow- Sneak peek](https://youtu.be/iy6RDozAM4A)\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#3202](https://github.com/derailed/k9s/issues/3202) 0.40.8 breaks plugins loading\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.40.11.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.40.11\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.40.0 -Column Blow- Sneak peek](https://youtu.be/iy6RDozAM4A)\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#3226](https://github.com/derailed/k9s/issues/3226) Filter view will show mess when filtering some string\n* [#3224](https://github.com/derailed/k9s/issues/3224) Respect kubectl.kubernetes.io/default-container annotation\n* [#3222](https://github.com/derailed/k9s/issues/3222) Option to Display Resource Names Without API Version Prefix\n* [#3210](https://github.com/derailed/k9s/issues/3210) Description line is buggy\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#3237](https://github.com/derailed/k9s/pull/3237) fix: List CRDs which has k8s.io in their names\n* [#3223](https://github.com/derailed/k9s/pull/3223) Fixed skin config ref of in_the_navy to in-the-navy\n* [#3110](https://github.com/derailed/k9s/pull/3110) feat: add splashless option to suppress splash screen on start\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.40.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.40.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n😳 Aye! Buzz kill on the 0.40.0 aftermath ;( Hot fix in progress...🙀 👻\n\nLikely additional `disturbance in the farce` might be observed.\nThank you all for giving this drop a rinse and reporting back!! 😍\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.40.0 -Column Blow- Sneak peek](https://youtu.be/iy6RDozAM4A)\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#3116](https://github.com/derailed/k9s/issues/3116) Cannot list custom CRD's since v0.40.1\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.40.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.40.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n😳 Aye! Buzz kill on the 0.40.0 aftermath ;( Hot fix in progress...🙀 👻\n\nLikely additional `disturbance in the farce` might be observed.\nThank you all for giving this drop a rinse and reporting back!! 😍\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.40.0 -Column Blow- Sneak peek](https://youtu.be/iy6RDozAM4A)\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#3116](https://github.com/derailed/k9s/issues/3116) Cannot list custom CRD's since v0.40.1 (with feelings!)\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.40.4.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.40.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n😳 Aye! Continued Buzz kill on the 0.40.0 aftermath 🙀 👻\n\nLikely additional `disturbance in the farce` might be observed.\nThank you all for giving this drop a rinse and reporting back!! 😍\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.40.0 -Column Blow- Sneak peek](https://youtu.be/iy6RDozAM4A)\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#3122](https://github.com/derailed/k9s/issues/3122) Viewing events is no longer sorted by LAST SEEN\n* [#3120](https://github.com/derailed/k9s/issues/3120) Custom View Column Mismatch in K9s: Shuffled Values in Pods View\n* [#3119](https://github.com/derailed/k9s/issues/3119) Custom Views Fail to Load with % in Column Names\n* [#3118](https://github.com/derailed/k9s/issues/3118)  selecting an alias, the wrong resources are being shown\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#3123](https://github.com/derailed/k9s/pull/3123) update regex to allow '%' and '/' in column names\n\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.40.5.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.40.5\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n😳 Aye! Continued Buzz kill on the 0.40.0 aftermath 🙀 👻\n\nLikely additional `disturbance in the farce` might be observed.\nThank you all for giving this drop a rinse and reporting back!! 😍\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.40.0 -Column Blow- Sneak peek](https://youtu.be/iy6RDozAM4A)\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#3131](https://github.com/derailed/k9s/issues/3131) Singular versions of native Kubernetes resource names no longer work\n* [#3119](https://github.com/derailed/k9s/issues/3119) Custom Views Fail to Load with % in Column Names (with feelings!)\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#3123](https://github.com/derailed/k9s/pull/3123) update regex to allow '%' and '/' in column names\n\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.40.6.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.40.6\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n### Breaking change\n\nMoved `portForwardAddress` out of clusterXXX/contextYYY/config.yaml and into the main K9s config file.\nThis is a global preference based on your setup vs a cluster/context specific attribute.\nK9s will nag you in the logs if a specific context config still contains this attribute but should not prevent the configuration load.\n\n### Column Blow Reloaded!\n\nWe've added another property to the custom view. You can now also specify namespace specific column definition for a given resource.\nFor instance, view pods in any namespace using one configuration and view pods in `fred` namespace using an alternate configuration.\n\n```yaml\n# views.yaml\nviews:\n  # Using this for all pods...\n  v1/pods:\n    columns:\n      - AGE\n      - NAMESPACE|WR                                     # => 🌚 Specifies the NAMESPACE column to be right aligned and only visible while in wide mode\n      - ZORG:.metadata.labels.fred\\.io\\.kubernetes\\.blee # => 🌚 extract fred.io.kubernetes.blee label into it's own column\n      - BLEE:.metadata.annotations.blee|R                # => 🌚 extract annotation blee into it's own column and right align it\n      - NAME\n      - IP\n      - NODE\n      - STATUS\n      - READY\n      - MEM/RL|S                                         # => 🌚 Overrides std resource default wide attribute via `S` for `Show`\n      - '%MEM/R|'                                        # => NOTE! column names with non alpha names need to be quoted as columns must be strings!\n\n   # Use this instead for pods in namespace `fred`\n   v1/pods@fred:                                         # => 🌚 New v0.40.6! Customize columns for a given resource and namespace!\n    columns:\n      - AGE\n      - NAMESPACE|WR\n```\n\nAdditionally, we've added a new column attribute aka `Show` -> `S`. This allows you to now override the default resource column `wide` attribute when set.\n\n\n---\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.40.0 -Column Blow- Sneak peek](https://youtu.be/iy6RDozAM4A)\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#3179](https://github.com/derailed/k9s/issues/3179) Resource name with full api or group displayed (somewhere and sometimes)\n* [#3178](https://github.com/derailed/k9s/issues/3178) Cronjobs with the same name in different namespaces appear together\n* [#3176](https://github.com/derailed/k9s/issues/3176) Trigger all marked cronjobs\n* [#3162](https://github.com/derailed/k9s/issues/3162) Context configs: context directory created under wrong cluster after context switch\n* [#3161](https://github.com/derailed/k9s/issues/3161) Force wide-only columns to appear outside of wide view\n* [#3147](https://github.com/derailed/k9s/issues/3147) Prompt style is overriden by body\n* [#3139](https://github.com/derailed/k9s/issues/3139) CPU/R:L and MEM/R:L columns invalid in views.yaml\n* [#3138](https://github.com/derailed/k9s/issues/3138) Subresources are not shown correctly in the RBAC view\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#3182](https://github.com/derailed/k9s/pull/3182) fix: Use the latest version when downloading the Ubuntu deb file\n* [#3168](https://github.com/derailed/k9s/pull/3168) fix(history): handle cases where special commands add their command their command to the history\n* [#3159](https://github.com/derailed/k9s/pull/3159) Added hard contrast gruvbox skins\n* [#3149](https://github.com/derailed/k9s/pull/3149) fix: Pass grv on gotoResource as a String to fix non-default apiGroup list\n* [#3149](https://github.com/derailed/k9s/pull/3149) Add externalsecrets plugin\n* [#3140](https://github.com/derailed/k9s/pull/3140) fix: Avoid false positive matches in enableRegion (#3093)\n\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.40.7.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.40.7\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n🙀 Hoy! Hosed custom view loading in v0.40.6...\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.40.0 -Column Blow- Sneak peek](https://youtu.be/iy6RDozAM4A)\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#3186](https://github.com/derailed/k9s/pull/3186) fix: allow absolute paths for the 'dir' command\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.40.8.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.40.8\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.40.0 -Column Blow- Sneak peek](https://youtu.be/iy6RDozAM4A)\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#3193](https://github.com/derailed/k9s/issues/3193) Feature Request: View aliases with custom columns\n* [#3192](https://github.com/derailed/k9s/issues/3192) Allow readonly indicator respect the noIcons configuration\n* [#3153](https://github.com/derailed/k9s/issues/3153) Add support for bunyan logging\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#3186](https://github.com/derailed/k9s/pull/3186) fix: allow absolute paths for the 'dir' command\n* [#3152](https://github.com/derailed/k9s/pull/3152) Feat: Add plugin support for parsing logs with bunyan cli #3153\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.40.9.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.40.9\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## Maintenance Release!\n\n* Refactored plugins implementation, hopefully we didn't hose them 😳\n* Updated plugins docs\n* Apparently when it comes to icons, I've chosen... poorly 🙀\n  Updated `write` icon 🔓->✍️, hopefully for the better 👀??\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.40.0 -Column Blow- Sneak peek](https://youtu.be/iy6RDozAM4A)\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#3202](https://github.com/derailed/k9s/issues/3202) 0.40.8 breaks plugins loading\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.50.0.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.50\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\n* [Afterimage - Justice](https://www.youtube.com/watch?v=9zBJlLbkfzA)\n* [This Is The Day - The The](https://www.youtube.com/watch?v=qBF3YqUzYRc)\n\n## 5-O, 5-0... Spring Cleaning In Effect!\n\n☠️ Careful on this upgrade! 🏴‍☠️\nWe've gone thru lots of code revamp/refactor on this drop, so mileage may vary!!\n\n### K9s Slow?\n\nIt looks like K9s performance took a dive in the wrong direction circa v0.40.x releases.\nTook a big perf/cleanup pass to improve perf and think this release should help a lot (famous last words...)\n\n> NOTE! As my dear granny use to say: `You can't cook a great meal without trashing the kitchen`,\n> So likely I have broken a few things in the process. So thread carefully and report back!\n\n### Now with Super Column Blow!\n\nBy general demand, juice up custom views! In a feature we like to refer to as `Super Column Blow...`\nAs of this drop, you can go full `Chuck Norris` and sprinkle some of your JQ_FU with you custom views.\n\nFor example...\n\n```yaml\n# views.yaml\nviews:\n  v1/pods:\n    sortColumn: NAME:asc\n    columns:\n    - AGE\n    - NAMESPACE\n    - NAME\n    - IMG-VERSION:.spec.containers[0].image|split(\":\")|.[-1]|R # => Grab the main container image name and pull the image version\n                                                               # => out into the `IMG-VERSION` right aligned column\n```\n\n> NOTE: ☢️ This is very much experimental! Not all JQ queries features are supported!\n> (See https://github.com/itchyny/gojq for the details!)\n\n## Videos Are In The Can!\n\nPlease dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content...\n\n* [K9s v0.40.0 -Column Blow- Sneak peek](https://youtu.be/iy6RDozAM4A)\n* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE)\n* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4)\n* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU)\n\n---\n\n## Resolved Issues\n\n* [#3226](https://github.com/derailed/k9s/issues/3226) Filter view will show mess when filtering some string\n* [#3224](https://github.com/derailed/k9s/issues/3224) Respect kubectl.kubernetes.io/default-container annotation\n* [#3222](https://github.com/derailed/k9s/issues/3222) Option to Display Resource Names Without API Version Prefix\n* [#3210](https://github.com/derailed/k9s/issues/3210) Description line is buggy\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#3237](https://github.com/derailed/k9s/pull/3237) fix: List CRDs which has k8s.io in their names\n* [#3223](https://github.com/derailed/k9s/pull/3223) Fixed skin config ref of in_the_navy to in-the-navy\n* [#3110](https://github.com/derailed/k9s/pull/3110) feat: add splashless option to suppress splash screen on start\n\n---\n\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.50.1.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.51\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## 5-0, 5-0 HotFix!\n\nIt looks like we've broken a few things in the clean up process 😳\nApologizes for the `disruption in the farce`. Hopefully happier on v0.50.1...\nCrossing fingers and toes!\n\n☠️ Careful on this upgrade! 🏴‍☠️\nWe've gone thru lots of code revamp/refactor in the v0.50.0, so mileage may vary...\n\n---\n\n## Resolved Issues\n\n* [#3262](https://github.com/derailed/k9s/issues/3262) Crash when no shellPod is defined in config file\n* [#3261](https://github.com/derailed/k9s/issues/3261) aliases with namespace and/or labels produce an error\n* [#3258](https://github.com/derailed/k9s/issues/3258) mac silicon 0.50.0 runtime error\n* [#3257](https://github.com/derailed/k9s/issues/3257) pods are reported to run on nodes they are not running on\n* [#3256](https://github.com/derailed/k9s/issues/3256) Pods view seems broken in 0.50.0\n* [#3255](https://github.com/derailed/k9s/issues/3255) Custom view does not work randomly\n\n\n---\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.50.10.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.50.10\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/zt-3360a389v-ElLHrb0Dp1kAXqYUItSAFA)\n\n## Maintenance Release!\n\n---\n\n## A Word From Our Sponsors...\n\nTo all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!!\n\n* [rufusshrestha](https://github.com/rufusshrestha)\n* [Ovidijus Balkauskas](https://github.com/Stogas)\n* [Konrad Konieczny](https://github.com/Psyhackological)\n* [Serit Tromsø](https://github.com/serit)\n* [Dennis](https://github.com/dennisTGC)\n* [LinPr](https://github.com/LinPr)\n* [franzXaver987](https://github.com/franzXaver987)\n* [Drew Showalter](https://github.com/one19)\n* [Sandylen](https://github.com/Sandylen)\n* [Uriah Carpenter](https://github.com/uriahcarpenter)\n* [Vector Group](https://github.com/vectorgrp)\n* [Stefan Roman](https://github.com/katapultcloud)\n* [Phillip](https://github.com/Loki-Afro)\n* [Lasse Bang Mikkelsen](https://github.com/lassebm)\n\n> Sponsorship cancellations since the last release: **19!** 🥹\n\n---\n\n## Resolved Issues\n\n* [#3541](https://github.com/derailed/k9s/issues/3541) ServiceAccount RBAC Rules not displayed if RoleBinding subject doesn't specify namespace\n* [#3535](https://github.com/derailed/k9s/issues/3535) Current Release process will cause code changes been reverted\n* [#3525](https://github.com/derailed/k9s/issues/3525) k9s suspends when launching foreground plugin\n* [#3495](https://github.com/derailed/k9s/issues/3495) Regression: filtering no long works with aliases\n* [#3478](https://github.com/derailed/k9s/issues/3478) High Disk and CPU usage when imageScans Is enabled in K9s\n* [#3470](https://github.com/derailed/k9s/issues/3470) Aliases for pods with unequal (!=) label filters not working\n* [#3466](https://github.com/derailed/k9s/issues/3466) Shared GPU (nvidia.com/gpu.shared) is shown as n/a on K9s node view\n* [#3455](https://github.com/derailed/k9s/issues/3455) memory command not found\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#3558](https://github.com/derailed/k9s/pull/3558) refactor(duplik8s): consolidate duplicate resource commands and updat…\n* [#3555](https://github.com/derailed/k9s/pull/3555) feat: add dup plugin\n* [#3543](https://github.com/derailed/k9s/pull/3543) Make \"flux trace\" more generic\n* [#3536](https://github.com/derailed/k9s/pull/3536) Add flux-operator resources to flux plugin\n* [#3528](https://github.com/derailed/k9s/pull/3528) feat(plugins): add pvc debug container plugin\n* [#3517](https://github.com/derailed/k9s/pull/3517) Feature/refresh rate\n* [#3516](https://github.com/derailed/k9s/pull/3516) Fixes flickering/jumping issue in context suggestions caused by inconsistent spacing behavior\n* [#3515](https://github.com/derailed/k9s/pull/3515) Fix/suppress init no resources warning\n* [#3513](https://github.com/derailed/k9s/pull/3513) fix: Color PV row according to its STATUS column\n* [#3513](https://github.com/derailed/k9s/pull/3513) fix: Color PV row according to its STATUS column\n* [#3505](https://github.com/derailed/k9s/pull/3505) docs: Add installation method with gah\n* [#3503](https://github.com/derailed/k9s/pull/3503) fix(logs): enhance log streaming with retry mechanism and error handling\n* [#3489](https://github.com/derailed/k9s/pull/3489) feat: Add context deletion functionality\n* [#3487](https://github.com/derailed/k9s/pull/3487) fsupport core group resources in k9s/plugins/watch-events.yaml\n* [#3485](https://github.com/derailed/k9s/pull/3485) Add disable-self-subject-access-reviews flag to disable can-i check…\n* [#3464](https://github.com/derailed/k9s/pull/3464) fix: get-all command in get all plugin\n\n---\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)#"
  },
  {
    "path": "change_logs/release_v0.50.11.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.50.11\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/zt-3360a389v-ElLHrb0Dp1kAXqYUItSAFA)\n\n## Maintenance Release!\n\nOh dear! Hopefully we're happier on this drop?? Apologizes for the `disturbance in the farce`...\n\n## Resolved Issues\n\n* [#3567](https://github.com/derailed/k9s/issues/3567) Extra slash '/' added when filtering from the command prompt\n* [#3566](https://github.com/derailed/k9s/issues/3566) unable to switch context or use k9s after upgrade to 0.50.10\n\n---\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)#"
  },
  {
    "path": "change_logs/release_v0.50.12.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.50.12\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/zt-3360a389v-ElLHrb0Dp1kAXqYUItSAFA)\n\n## Maintenance Release!\n\n## Resolved Issues\n\n* [#3570](https://github.com/derailed/k9s/issues/3570) 0.50.11 could not display any resources\n* [#3562](https://github.com/derailed/k9s/issues/3562) Can't delete namespace\n* [#3547](https://github.com/derailed/k9s/issues/3547) Error message from admission controller\n\n---\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)#"
  },
  {
    "path": "change_logs/release_v0.50.13.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.50.13\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/zt-3360a389v-ElLHrb0Dp1kAXqYUItSAFA)\n\n## Maintenance Release!\n\n## Resolved Issues\n\n* [#3587](https://github.com/derailed/k9s/issues/3587) UI doesn't show any updates when restarting a Deployment\n* [#3585](https://github.com/derailed/k9s/issues/3585) abbreviation sec for secret not working\n* [#3584](https://github.com/derailed/k9s/issues/3584) Show managed fields doesn't show them\n* [#3583](https://github.com/derailed/k9s/issues/3583) Cannot open shell to pods without node read access as of 0.50.12\n* [#3577](https://github.com/derailed/k9s/issues/3577) Log view is broken as of v0.50.10\n* [#3574](https://github.com/derailed/k9s/issues/3574) Aliases for pods with label filters not working\n\n---\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)#"
  },
  {
    "path": "change_logs/release_v0.50.14.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.50.14\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by big corporations with deep pockets, thus if you feel K9s is helping in your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/zt-3360a389v-ElLHrb0Dp1kAXqYUItSAFA)\n\n## Maintenance Release!\n\nSponsorships are dropping at an alarming rate which puts this project in the red. This is becoming a concern and sad not to mention unsustainable ;( If you dig `k9s` and want to help the project, please consider `paying it forward!` and don't become just another `satisfied, non paying customer!`. K9s does take a lot of my `free` time to maintain, enhance and keep the light on. Many cool ideas are making it straight to the `freezer` as I just can't budget them in.\nI know many of you work for big corporations, so please put in the word/work and have them help us out via sponsorships or other means.\n\nThank you!\n\n## Resolved Issues\n\n* [#3608](https://github.com/derailed/k9s/issues/3608) k9s crashes when :namespaces used\n* [#3606](https://github.com/derailed/k9s/issues/3606) Xray not working anymore on (possible) v0.50.X\n* [#3594](https://github.com/derailed/k9s/issues/3594) Show pod yaml - Boom!! cannot deep copy int\n* [#3591](https://github.com/derailed/k9s/issues/3591) Accept suggestion with enter (without having to \"tab\")\n* [#3576](https://github.com/derailed/k9s/issues/3576) Custom alias/view not working anymore since v0.50.10\n\n---\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)#"
  },
  {
    "path": "change_logs/release_v0.50.15.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.50.15\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by big corporations with deep pockets, thus if you feel K9s is helping in your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/zt-3360a389v-ElLHrb0Dp1kAXqYUItSAFA)\n\n## Maintenance Release!\n\nSponsorships are dropping at an alarming rate which puts this project in the red. This is becoming a concern and sad not to mention unsustainable ;( If you dig `k9s` and want to help the project, please consider `paying it forward!` and don't become just another `satisfied, non paying customer!`. K9s does take a lot of my `free` time to maintain, enhance and keep the light on. Many cool ideas are making it straight to the `freezer` as I just can't budget them in.\nI know many of you work for big corporations, so please put in the word/work and have them help us out via sponsorships or other means.\n\nThank you!\n\n## Resolved Issues\n\n* [#3591](https://github.com/derailed/k9s/issues/3591) REVERTED! Accept suggestion with enter (without having to \"tab\")\n\n---\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)#"
  },
  {
    "path": "change_logs/release_v0.50.16.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.50.16\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by big corporations with deep pockets, thus if you feel K9s is helping in your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/zt-3360a389v-ElLHrb0Dp1kAXqYUItSAFA)\n\n## Maintenance Release!\n\nSponsorships are dropping at an alarming rate which puts this project in the red. This is becoming a concern and sad not to mention unsustainable ;( If you dig `k9s` and want to help the project, please consider `paying it forward!` and don't become just another `satisfied, non paying customer!`. K9s does take a lot of my `free` time to maintain, enhance and keep the light on. Many cool ideas are making it straight to the `freezer` as I just can't budget them in.\nI know many of you work for big corporations, so please put in the word/work and have them help us out via sponsorships or other means.\n\nThank you!\n\n### Warp Speed Scotty!\n\nAs of this drop, we are introducing `namespace warp` via shortcut `w`.\nThis affords to view all resources of that type based on the currently selected resource namespace.\nThis command is only available on namespaced resources.\nFor example, if you are in pod view and select pod-xxx in namespace `bozo`, hitting `w` will `warp`\nyou to view all pods in namespace `bozo`.\n\n## Resolved Issues\n\n* [#3629](https://github.com/derailed/k9s/issues/3629) vulnerability in k9s project\n* [#3621](https://github.com/derailed/k9s/issues/3621) Switching to \":Deploy\" sends you to deployments from namespace \"deploy\"\n* [#3620](https://github.com/derailed/k9s/issues/3620) Trying to show pod yaml using custom views.yaml crashes k9s\n* [#3608](https://github.com/derailed/k9s/issues/3608) k9s crashes when :namespaces used\n* [#3601](https://github.com/derailed/k9s/issues/3601) Can't delete namespace\n* [#3595](https://github.com/derailed/k9s/issues/3595) Toggle Namespace Filter in Pods View with 'n' Key\n* [#3576](https://github.com/derailed/k9s/issues/3576) Custom alias/view not working anymore since v0.50.10\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#3625](https://github.com/derailed/k9s/pull/3625) fix: debug-container plugin when KUBECONFIG has multiple files\n* [#3623](https://github.com/derailed/k9s/pull/3623) bugfix: fix panic in BenchmarkPodRender by using NewPod() constructor\n* [#3619](https://github.com/derailed/k9s/pull/3619) feat: plugin to list all resources by namespace\n* [#3605](https://github.com/derailed/k9s/pull/3605) browser: do not prevent redraw when connection unavailable\n* [#3600](https://github.com/derailed/k9s/pull/3600) fix(shell): set linux when OS detection fails\n* [#3588](https://github.com/derailed/k9s/pull/3588) fix: do not error out of shellIn if OS detection fails\n\n\n---\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)#"
  },
  {
    "path": "change_logs/release_v0.50.17.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.50.18\n\n## Notes\n\n🥳🎉 Happy new year fellow k9ers!🎊🍾 Hoping 2026 will bring good health and great success to you and yours...\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by big corporations with deep pockets, thus if you feel K9s is helping in your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/zt-3360a389v-ElLHrb0Dp1kAXqYUItSAFA)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\n* [A cool new way - Joe Satriani](https://www.youtube.com/watch?v=4apA948yOF0)\n* [Song for you - Ray Charles](https://www.youtube.com/watch?v=CzAkTrDiXxg)\n* [Kill the pain - SYZGYX](https://www.youtube.com/watch?v=5XuvMhHZorw&list=RD5XuvMhHZorw&start_radio=1)\n\n---\n\n## Maintenance Release!\n\nSponsorships are dropping at an alarming rate which puts this project in the red. This is becoming a concern and sad not to mention unsustainable ;( If you dig `k9s` and want to help the project, please consider `paying it forward!` and don't become just another `satisfied, non paying customer!`. K9s does take a lot of my `free` time to maintain, enhance and keep the light on. Many cool ideas are making it straight to the `freezer` as I just can't budget them in.\nI know many of you work for big corporations, so please put in the word/work and have them help us out via sponsorships or other means.\n\nThank you!\n\n\n## A Word From Our Sponsors...\n\nTo all the good folks and orgs below that opted to `pay it forward` and join our sponsorship program, I salute you!!\n\n* [Philomena Yeboah](https://github.com/PhilomenaYeboah1989)\n* [Kilian](https://github.com/kaerbr)\n* [TVRiddle](https://github.com/TVRiddle)\n* [Tom Morelly](https://github.com/FalcoSuessgott)\n* [Nikhil Narayen](https://github.com/nnarayen)\n* [Andrew Aadland](https://github.com/DaemonDude23)\n* [Radek](https://github.com/radvym)\n* [Timothée Gerber](https://github.com/TimotheeGerber)\n* [Matthias](https://github.com/maetthu)\n* [DKB](https://github.com/dkb-bank) ❤️\n* [Kraken Tech](https://github.com/kraken-tech)\n* [Daniel](https://github.com/sherlock7402)\n* [Fred Loucks](https://github.com/fullmetal-fred)\n* [Patricia Mascaros](https://github.com/ccong2586)\n* [Qube Research & Technologies](https://github.com/qube-rt)\n* [Michel Jung](https://github.com/micheljung)\n* [Ümüt Özalp](https://github.com/uozalp)\n* [Nathan Papapietro](https://github.com/npapapietro)\n* [Oleksandr Podze](https://github.com/dasdy)\n* [Lee Jones](https://github.com/leejones)\n* [tsahlif](https://github.com/tshalif)\n* [Jean-Christophe Amiel](https://github.com/jcamiel)\n* [Lightspark](https://github.com/lightsparkdev)\n* [egs-hub](https://github.com/egs-hub) ❤️\n* [Sergey](https://github.com/malsatin)\n* [Wynter Inc](https://github.com/copytesting)\n* [Jen Norris](https://github.com/tnorris)\n* [Joakim-Byg](https://github.com/Joakim-Byg)\n* [Oleksandr Podze](https://github.com/dasdy)\n* [Lee Jones](https://github.com/leejones)\n\n> Sponsorship cancellations since the last release: **17!** 🥹\n\n## Resolved Issues\n\n* [#3765](https://github.com/derailed/k9s/issues/3765) quay.io docker images not up to date but referenced in README.md\n* [#3762](https://github.com/derailed/k9s/issues/3762) Copy multiple selected items\n* [#3751](https://github.com/derailed/k9s/issues/3751) Improve visual distinction for cordoned nodes in Node view\n* [#3735](https://github.com/derailed/k9s/issues/3735) Cannot decode secret if there is no get permissions for all secrets\n* [#3708](https://github.com/derailed/k9s/issues/3708) Editing a single Namespace opens the editor with a list of all Namespaces\n* [#3731](https://github.com/derailed/k9s/issues/3731) feat: add neat plugin\n\n* [#3735](https://github.com/derailed/k9s/issues/3735) Cannot decode secret if there is no get permissions for all secrets\n* [#3708](https://github.com/derailed/k9s/issues/3708) Editing a single Namespace opens the editor with a list of all Namespaces\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#3763](https://github.com/derailed/k9s/pull/3763) feat: enable copying multiple resource, namespace names to clipboard\n* [#3760](https://github.com/derailed/k9s/pull/3760) fix: Editing a single Namespace opens the editor with a list of all Namespaces\n* [#3756](https://github.com/derailed/k9s/pull/3756) feat: Add reconcile plugin for Flux instances\n* [#3755](https://github.com/derailed/k9s/pull/3755) fix: panic on 'jump to owner' of reflect.Value.Elem on zero Value\n* [#3753](https://github.com/derailed/k9s/pull/3553) feat: add plugins for argo workflows\n* [#3750](https://github.com/derailed/k9s/pull/3750) fix: Flux trace plugin shortcut conflict by changing to Shift-Q\n* [#3749](https://github.com/derailed/k9s/pull/3749) feat: add dark/light theme inversion using Oklch\n* [#3739](https://github.com/derailed/k9s/pull/3739) chore: refine LabelsSelector comment to match function behavior\n* [#3738](https://github.com/derailed/k9s/pull/3738) feat: add symlink handle for plugin directory\n* [#3720](https://github.com/derailed/k9s/pull/3720) fix(internal/render): ensure object is deep copied before realization in Render method\n* [#3704](https://github.com/derailed/k9s/pull/3704) Allow k9s to start without a valid Kubernetes context\n* [#3699](https://github.com/derailed/k9s/pull/3699) feat(pulse): map hjkl to navigate as help shows\n* [#3697](https://github.com/derailed/k9s/pull/3697) Issue 3667 Fix\n* [#3696](https://github.com/derailed/k9s/pull/3696) fix for scale option appearing on non-scalable resources\n* [#3690](https://github.com/derailed/k9s/pull/3690) feat: add support for scaling HPA targets\n* [#3671](https://github.com/derailed/k9s/pull/3671) fix fails to modify or delete namespaces using RBAC\n* [#3669](https://github.com/derailed/k9s/pull/3669) feat: logs column lock\n* [#3663](https://github.com/derailed/k9s/pull/3663) Map Q to \"Back\"\n* [#3859](https://github.com/derailed/k9s/pull/3859) fix: update busybox image version to 1.37.0 in configuration files\n* [#3650](https://github.com/derailed/k9s/pull/3650) Sort all columns\n* [#3458](https://github.com/derailed/k9s/pull/3458) Document how to install on Fedora\n\n---\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2026 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)#"
  },
  {
    "path": "change_logs/release_v0.50.18.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.50.18\n\n## Notes\n\n🥳🎉 Happy new year fellow k9ers!🎊🍾 Hoping 2026 will bring good health and great success to you and yours...\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by big corporations with deep pockets, thus if you feel K9s is helping in your Kubernetes journey, please consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/zt-3360a389v-ElLHrb0Dp1kAXqYUItSAFA)\n\n---\n\n## ♫ Sounds Behind The Release ♭\n\n* [A cool new way - Joe Satriani](https://www.youtube.com/watch?v=4apA948yOF0)\n* [Song for you - Ray Charles](https://www.youtube.com/watch?v=CzAkTrDiXxg)\n* [Kill the pain - SYZGYX](https://www.youtube.com/watch?v=5XuvMhHZorw&list=RD5XuvMhHZorw&start_radio=1)\n\n---\n\n## Maintenance Release!\n\nOops! I've missed a PR in the v0.50.17 excitement ;( Dropping v0.50.18 with feelings...\n\nSponsorships are dropping at an alarming rate which puts this project in the red. This is becoming a concern and sad not to mention unsustainable ;( If you dig `k9s` and want to help the project, please consider `paying it forward!` and don't become just another `satisfied, non paying customer!`. K9s does take a lot of my `free` time to maintain, enhance and keep the light on. Many cool ideas are making it straight to the `freezer` as I just can't budget them in.\nI know many of you work for big corporations, so please put in the word/work and have them help us out via sponsorships or other means.\n\nThank you!\n\n---\n\n## A Word From Our Sponsors...\n\nTo all the good folks and orgs below that opted to `pay it forward` and join our sponsorship program, I salute you!!\n\n* [Philomena Yeboah](https://github.com/PhilomenaYeboah1989)\n* [Kilian](https://github.com/kaerbr)\n* [TVRiddle](https://github.com/TVRiddle)\n* [Tom Morelly](https://github.com/FalcoSuessgott)\n* [Nikhil Narayen](https://github.com/nnarayen)\n* [Andrew Aadland](https://github.com/DaemonDude23)\n* [Radek](https://github.com/radvym)\n* [Timothée Gerber](https://github.com/TimotheeGerber)\n* [Matthias](https://github.com/maetthu)\n* [DKB](https://github.com/dkb-bank) ❤️\n* [Kraken Tech](https://github.com/kraken-tech)\n* [Daniel](https://github.com/sherlock7402)\n* [Fred Loucks](https://github.com/fullmetal-fred)\n* [Patricia Mascaros](https://github.com/ccong2586)\n* [Qube Research & Technologies](https://github.com/qube-rt)\n* [Michel Jung](https://github.com/micheljung)\n* [Ümüt Özalp](https://github.com/uozalp)\n* [Nathan Papapietro](https://github.com/npapapietro)\n* [Oleksandr Podze](https://github.com/dasdy)\n* [Lee Jones](https://github.com/leejones)\n* [tsahlif](https://github.com/tshalif)\n* [Jean-Christophe Amiel](https://github.com/jcamiel)\n* [Lightspark](https://github.com/lightsparkdev)\n* [egs-hub](https://github.com/egs-hub) ❤️\n* [Sergey](https://github.com/malsatin)\n* [Wynter Inc](https://github.com/copytesting)\n* [Jen Norris](https://github.com/tnorris)\n* [Joakim-Byg](https://github.com/Joakim-Byg)\n* [Oleksandr Podze](https://github.com/dasdy)\n* [Lee Jones](https://github.com/leejones)\n\n> Sponsorship cancellations since the last release: **17!** 🥹\n\n## Resolved Issues\n\n* [#3765](https://github.com/derailed/k9s/issues/3765) quay.io docker images not up to date but referenced in README.md\n* [#3762](https://github.com/derailed/k9s/issues/3762) Copy multiple selected items\n* [#3751](https://github.com/derailed/k9s/issues/3751) Improve visual distinction for cordoned nodes in Node view\n* [#3735](https://github.com/derailed/k9s/issues/3735) Cannot decode secret if there is no get permissions for all secrets\n* [#3708](https://github.com/derailed/k9s/issues/3708) Editing a single Namespace opens the editor with a list of all Namespaces\n* [#3731](https://github.com/derailed/k9s/issues/3731) feat: add neat plugin\n* [#3735](https://github.com/derailed/k9s/issues/3735) Cannot decode secret if there is no get permissions for all secrets\n* [#3708](https://github.com/derailed/k9s/issues/3708) Editing a single Namespace opens the editor with a list of all Namespaces\n* [#3649](https://github.com/derailed/k9s/issues/3649) Improved Column Sorting\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#3763](https://github.com/derailed/k9s/pull/3763) feat: enable copying multiple resource, namespace names to clipboard\n* [#3760](https://github.com/derailed/k9s/pull/3760) fix: Editing a single Namespace opens the editor with a list of all Namespaces\n* [#3756](https://github.com/derailed/k9s/pull/3756) feat: Add reconcile plugin for Flux instances\n* [#3755](https://github.com/derailed/k9s/pull/3755) fix: panic on 'jump to owner' of reflect.Value.Elem on zero Value\n* [#3753](https://github.com/derailed/k9s/pull/3553) feat: add plugins for argo workflows\n* [#3750](https://github.com/derailed/k9s/pull/3750) fix: Flux trace plugin shortcut conflict by changing to Shift-Q\n* [#3749](https://github.com/derailed/k9s/pull/3749) feat: add dark/light theme inversion using Oklch\n* [#3739](https://github.com/derailed/k9s/pull/3739) chore: refine LabelsSelector comment to match function behavior\n* [#3738](https://github.com/derailed/k9s/pull/3738) feat: add symlink handle for plugin directory\n* [#3720](https://github.com/derailed/k9s/pull/3720) fix(internal/render): ensure object is deep copied before realization in Render method\n* [#3704](https://github.com/derailed/k9s/pull/3704) Allow k9s to start without a valid Kubernetes context\n* [#3699](https://github.com/derailed/k9s/pull/3699) feat(pulse): map hjkl to navigate as help shows\n* [#3697](https://github.com/derailed/k9s/pull/3697) Issue 3667 Fix\n* [#3696](https://github.com/derailed/k9s/pull/3696) fix for scale option appearing on non-scalable resources\n* [#3690](https://github.com/derailed/k9s/pull/3690) feat: add support for scaling HPA targets\n* [#3671](https://github.com/derailed/k9s/pull/3671) fix fails to modify or delete namespaces using RBAC\n* [#3669](https://github.com/derailed/k9s/pull/3669) feat: logs column lock\n* [#3663](https://github.com/derailed/k9s/pull/3663) Map Q to \"Back\"\n* [#3661](https://github.com/derailed/k9s/pull/3661) refactor: remove unused sorting key bindings from various views\n* [#3859](https://github.com/derailed/k9s/pull/3859) fix: update busybox image version to 1.37.0 in configuration files\n* [#3650](https://github.com/derailed/k9s/pull/3650) Sort all columns\n* [#3458](https://github.com/derailed/k9s/pull/3458) Document how to install on Fedora\n\n---\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2026 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)#"
  },
  {
    "path": "change_logs/release_v0.50.2.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.50.2\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM)\n\n## 5-0, 5-0 HotFix!\n\nIt looks like we've broken a few (more) things in the clean up process 😳\nThis is what you get for trying to refresh a ~10 year old code base 🙀\nApologizes for the `disruption in the farce`. Hopefully much happier on v0.50.2...\nAre we there yet? Crossing fingers AND toes...\n\n☠️ Careful on this upgrade! 🏴‍☠️\nWe've gone thru lots of code revamp/refactor in the v0.50.0, so mileage may vary...\n\n---\n\n## Resolved Issues\n\n* [#3267](https://github.com/derailed/k9s/issues/3267) Show some output or message when no resources are found\n* [#3266](https://github.com/derailed/k9s/issues/3266) Command alias :dp fails with \"no resource meta defined for deployments\" error\n* [#3264](https://github.com/derailed/k9s/issues/3264) can't execute get(y) or describe(d) in StorageClass view\n* [#3260](https://github.com/derailed/k9s/issues/3260) yaml view of pod will crash the app (Boom!! cannot deep copy int. (Maybe??)\n\n---\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.50.3.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.50.3\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/zt-3360a389v-ElLHrb0Dp1kAXqYUItSAFA)\n\n## Maintenance Release!\n\nA bit more code spring cleaning/TLC and address a few bugs:\n\n1. [RBAC View] Fix issue bombing out on RBAC cluster roles\n2. [Custom Views] Fix issue with parsing `jq` filters and bombing out (Big Thanks to Pierre for flagging it!)\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#3273](https://github.com/derailed/k9s/pull/3273) k9s plugin scopes containers issue\n* [#3169](https://github.com/derailed/k9s/pull/3169) feat: pass context and token flags to kubectl exec commands\n\n\n---\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.50.4.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.50.4\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/zt-3360a389v-ElLHrb0Dp1kAXqYUItSAFA)\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues\n\n* [#3288](https://github.com/derailed/k9s/issues/3288) Resource search doesn't filter by name in custom view\n* [#3286](https://github.com/derailed/k9s/issues/3286) K9S doesn't understand matchExpressions selector in Deployment to Pod navigation\n* [#3285](https://github.com/derailed/k9s/issues/3285) Rollout Restart method conflicts with GitOps (Flux, ArgoCD)\n* [#3283](https://github.com/derailed/k9s/issues/3283) Deployment status showing wrong ready state\n* [#3278](https://github.com/derailed/k9s/issues/3278) k9s doesn't honor the --namespace parameter\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#3292](https://github.com/derailed/k9s/pull/3292) fix: respect insecure flag when switch context\n* [#3277](https://github.com/derailed/k9s/pull/3277) feat: add hostPathVolume (docker)\n* [#3253](https://github.com/derailed/k9s/pull/3253) fix: set default request timeout to 120 seconds\n* [#2866](https://github.com/derailed/k9s/pull/2866) Feature/default_view\n\n---\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.50.5.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.50.5\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/zt-3360a389v-ElLHrb0Dp1kAXqYUItSAFA)\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues\n\n* [#3328](https://github.com/derailed/k9s/issues/3328) Pod overview shows wrong number of running containers with sidecar init-container\n* [#3309](https://github.com/derailed/k9s/issues/3309) [0.50.4] k9s crashes when attempting to load logs\n* [#3301](https://github.com/derailed/k9s/issues/3301) Port Forward deleted without UI notification when forwarding to wrong port\n* [#3294](https://github.com/derailed/k9s/issues/3294) [0.50.4] k9s crashes when filtering based on labels\n* [#3278](https://github.com/derailed/k9s/issues/3278) k9s doesn't honor the --namespace parameter\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#3311](https://github.com/derailed/k9s/pull/3311) Fix concurrent read writes\n* [#3310](https://github.com/derailed/k9s/pull/3310) fix: use full path of date to avoid conflict\n* [#3308](https://github.com/derailed/k9s/pull/3308) Show replicasets from deployment view\n* [#3300](https://github.com/derailed/k9s/pull/3300) fix: truncate label selector input to max length\n* [#3296](https://github.com/derailed/k9s/pull/3296) fix: update time format in logging to 24-hour format\n\n---\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.50.6.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.50.6\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/zt-3360a389v-ElLHrb0Dp1kAXqYUItSAFA)\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues\n\n* [#3334](https://github.com/derailed/k9s/issues/3334) Watcher failed for events.k8s.io/v1/events -- expecting a meta table but got *unstructured.Unstructure\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#3332](https://github.com/derailed/k9s/pull/3332) fix: pre-check for get permissions only on port-forward\n* [#3311](https://github.com/derailed/k9s/pull/3311) Fix concurrent read writes\n* [#3310](https://github.com/derailed/k9s/pull/3310) fix: use full path of date to avoid conflict\n\n---\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)"
  },
  {
    "path": "change_logs/release_v0.50.7.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.50.7\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/zt-3360a389v-ElLHrb0Dp1kAXqYUItSAFA)\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues\n\n* [#3435](https://github.com/derailed/k9s/issues/3435) noExitOnCtrlC\n* [#3434](https://github.com/derailed/k9s/issues/3434) Pulses - navigation selection is invisible\n* [#3424](https://github.com/derailed/k9s/issues/3424) feat: Add GPUs to nodes view\n* [#3422](https://github.com/derailed/k9s/issues/3422) Changing ns should keep current kind\n* [#3412](https://github.com/derailed/k9s/issues/3412) \"Toggle Decode\" for secret has no effect\n* [#3406](https://github.com/derailed/k9s/issues/3406) History navigation: new view after going back should truncate forward history\n* [#3398](https://github.com/derailed/k9s/issues/3398) Improve the UX of FieldManager field on restart\n* [#3383](https://github.com/derailed/k9s/issues/3383) Triggering a CronJob fails as Unauthorized since v0.50\n* [#3406](https://github.com/derailed/k9s/issues/3406) History navigation: new view after going back should truncate forward history\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#3433](https://github.com/derailed/k9s/pull/3433) feat(plugins): add kube-metrics plugin\n* [#3371](https://github.com/derailed/k9s/pull/3371) Add context to condition in keda-toggle plugin\n* [#3347](https://github.com/derailed/k9s/pull/3347) Fix GVR Title option in readme\n* [#3346](https://github.com/derailed/k9s/pull/3346) revert: #3322\n\n\n---\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)#"
  },
  {
    "path": "change_logs/release_v0.50.8.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.50.8\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/zt-3360a389v-ElLHrb0Dp1kAXqYUItSAFA)\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues\n\n* [#3453](https://github.com/derailed/k9s/issues/3453) [Feature Request] Add GPU column to pod/container view\n* [#3451](https://github.com/derailed/k9s/issues/3451) Weirdness when filtering namespaces\n* [#3439](https://github.com/derailed/k9s/issues/3438) Allow KnownGPUVendors customization\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#3437](https://github.com/derailed/k9s/pull/3437) feat: Add GPU usage to pod view\n* [#3421](https://github.com/derailed/k9s/pull/3421) Fix #3421 - can't switch namespaces in helm view\n* [#3356](https://github.com/derailed/k9s/pull/3356) allow skin to be selected via K9S_SKIN env var\n\n---\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)#"
  },
  {
    "path": "change_logs/release_v0.50.9.md",
    "content": "<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/k9s.png\" align=\"center\" width=\"800\" height=\"auto\"/>\n\n# Release v0.50.9\n\n## Notes\n\nThank you to all that contributed with flushing out issues and enhancements for K9s!\nI'll try to mark some of these issues as fixed. But if you don't mind grab the latest rev\nand see if we're happier with some of the fixes!\nIf you've filed an issue please help me verify and close.\n\nYour support, kindness and awesome suggestions to make K9s better are, as ever, very much noted and appreciated!\nAlso big thanks to all that have allocated their own time to help others on both slack and on this repo!!\n\nAs you may know, K9s is not pimped out by corps with deep pockets, thus if you feel K9s is helping your Kubernetes journey,\nplease consider joining our [sponsorship program](https://github.com/sponsors/derailed) and/or make some noise on social! [@kitesurfer](https://twitter.com/kitesurfer)\n\nOn Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/zt-3360a389v-ElLHrb0Dp1kAXqYUItSAFA)\n\n## Maintenance Release!\n\n---\n\n## Resolved Issues\n\n* [#3459](https://github.com/derailed/k9s/issues/3459) Update the tablewriter dependency + implementation\n* [#3458](https://github.com/derailed/k9s/issues/3458) Unable to switch namespaces with 0.50.8\n\n---\n\n## Contributed PRs\n\nPlease be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!!\n\n* [#3460](https://github.com/derailed/k9s/pull/3460) update to tablewriter v1 apis\n\n---\n<img src=\"https://raw.githubusercontent.com/derailed/k9s/master/assets/imhotep_logo.png\" width=\"32\" height=\"auto\"/> © 2025 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0)#"
  },
  {
    "path": "cmd/info.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\n\t\"github.com/derailed/k9s/internal/color\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/spf13/cobra\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc infoCmd() *cobra.Command {\n\treturn &cobra.Command{\n\t\tUse:   \"info\",\n\t\tShort: \"List K9s configurations info\",\n\t\tRunE:  printInfo,\n\t}\n}\n\nfunc printInfo(*cobra.Command, []string) error {\n\tif err := config.InitLocs(); err != nil {\n\t\treturn err\n\t}\n\n\tconst fmat = \"%-27s %s\\n\"\n\tprintLogo(color.Cyan)\n\tprintTuple(fmat, \"Version\", version, color.Cyan)\n\tprintTuple(fmat, \"Config\", config.AppConfigFile, color.Cyan)\n\tprintTuple(fmat, \"Custom Views\", config.AppViewsFile, color.Cyan)\n\tprintTuple(fmat, \"Plugins\", config.AppPluginsFile, color.Cyan)\n\tprintTuple(fmat, \"Hotkeys\", config.AppHotKeysFile, color.Cyan)\n\tprintTuple(fmat, \"Aliases\", config.AppAliasesFile, color.Cyan)\n\tprintTuple(fmat, \"Skins\", config.AppSkinsDir, color.Cyan)\n\tprintTuple(fmat, \"Context Configs\", config.AppContextsDir, color.Cyan)\n\tprintTuple(fmat, \"Logs\", config.AppLogFile, color.Cyan)\n\tprintTuple(fmat, \"Benchmarks\", config.AppBenchmarksDir, color.Cyan)\n\tprintTuple(fmat, \"ScreenDumps\", getScreenDumpDirForInfo(), color.Cyan)\n\n\treturn nil\n}\n\nfunc printLogo(c color.Paint) {\n\tfor _, l := range ui.LogoSmall {\n\t\t_, _ = fmt.Fprintln(out, color.Colorize(l, c))\n\t}\n\t_, _ = fmt.Fprintln(out)\n}\n\n// getScreenDumpDirForInfo get default screen dump config dir or from config.K9sConfigFile configuration.\nfunc getScreenDumpDirForInfo() string {\n\tif config.AppConfigFile == \"\" {\n\t\treturn config.AppDumpsDir\n\t}\n\n\tf, err := os.ReadFile(config.AppConfigFile)\n\tif err != nil {\n\t\tslog.Error(\"Unable to reads k9s config file\", slogs.Error, err)\n\t\treturn config.AppDumpsDir\n\t}\n\n\tvar cfg config.Config\n\tif err := yaml.Unmarshal(f, &cfg); err != nil {\n\t\tslog.Error(\"Unable to unmarshal k9s config file\", slogs.Error, err)\n\t\treturn config.AppDumpsDir\n\t}\n\tif cfg.K9s == nil {\n\t\treturn config.AppDumpsDir\n\t}\n\n\treturn cfg.K9s.AppScreenDumpDir()\n}\n"
  },
  {
    "path": "cmd/info_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage cmd\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_getScreenDumpDirForInfo(t *testing.T) {\n\ttests := map[string]struct {\n\t\tk9sConfigFile         string\n\t\texpectedScreenDumpDir string\n\t}{\n\t\t\"withK9sConfigFile\": {\n\t\t\tk9sConfigFile:         \"testdata/k9s.yaml\",\n\t\t\texpectedScreenDumpDir: \"/tmp\",\n\t\t},\n\t\t\"withEmptyK9sConfigFile\": {\n\t\t\tk9sConfigFile:         \"\",\n\t\t\texpectedScreenDumpDir: config.AppDumpsDir,\n\t\t},\n\t\t\"withInvalidK9sConfigFilePath\": {\n\t\t\tk9sConfigFile:         \"invalid\",\n\t\t\texpectedScreenDumpDir: config.AppDumpsDir,\n\t\t},\n\t\t\"withScreenDumpDirEmptyInK9sConfigFile\": {\n\t\t\tk9sConfigFile:         \"testdata/k9s1.yaml\",\n\t\t\texpectedScreenDumpDir: config.AppDumpsDir,\n\t\t},\n\t}\n\tfor k := range tests {\n\t\tu := tests[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tinitK9sConfigFile := config.AppConfigFile\n\t\t\tconfig.AppConfigFile = u.k9sConfigFile\n\n\t\t\tassert.Equal(t, u.expectedScreenDumpDir, getScreenDumpDirForInfo())\n\n\t\t\tconfig.AppConfigFile = initK9sConfigFile\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "cmd/root.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage cmd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"runtime/debug\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/color\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/lmittmann/tint\"\n\t\"github.com/mattn/go-colorable\"\n\t\"github.com/spf13/cobra\"\n\t\"k8s.io/cli-runtime/pkg/genericclioptions\"\n\t\"k8s.io/client-go/tools/clientcmd/api\"\n)\n\nconst (\n\tappName      = config.AppName\n\tshortAppDesc = \"A graphical CLI for your Kubernetes cluster management.\"\n\tlongAppDesc  = \"K9s is a CLI to view and manage your Kubernetes clusters.\"\n)\n\nvar _ data.KubeSettings = (*client.Config)(nil)\n\nvar (\n\tversion, commit, date = \"dev\", \"dev\", client.NA\n\tk9sFlags              *config.Flags\n\tk8sFlags              *genericclioptions.ConfigFlags\n\n\trootCmd = &cobra.Command{\n\t\tUse:   appName,\n\t\tShort: shortAppDesc,\n\t\tLong:  longAppDesc,\n\t\tRunE:  run,\n\t}\n\n\tout = colorable.NewColorableStdout()\n)\n\ntype flagError struct{ err error }\n\nfunc (e flagError) Error() string { return e.err.Error() }\n\nfunc init() {\n\tif err := config.InitLogLoc(); err != nil {\n\t\tfmt.Printf(\"Fail to init k9s logs location %s\\n\", err)\n\t}\n\n\trootCmd.SetFlagErrorFunc(func(_ *cobra.Command, err error) error {\n\t\treturn flagError{err: err}\n\t})\n\n\trootCmd.AddCommand(versionCmd(), infoCmd())\n\tinitK9sFlags()\n\tinitK8sFlags()\n}\n\n// Execute root command.\nfunc Execute() {\n\tif err := rootCmd.Execute(); err != nil {\n\t\tos.Exit(1)\n\t}\n}\n\nfunc run(*cobra.Command, []string) error {\n\tif err := config.InitLocs(); err != nil {\n\t\treturn err\n\t}\n\tlogFile, err := os.OpenFile(\n\t\t*k9sFlags.LogFile,\n\t\tos.O_CREATE|os.O_APPEND|os.O_WRONLY,\n\t\tdata.DefaultFileMod,\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"log file %q init failed: %w\", *k9sFlags.LogFile, err)\n\t}\n\tdefer func() {\n\t\tif logFile != nil {\n\t\t\t_ = logFile.Close()\n\t\t}\n\t}()\n\tdefer func() {\n\t\tif err := recover(); err != nil {\n\t\t\tslog.Error(\"Boom!! k9s init failed\", slogs.Error, err)\n\t\t\tslog.Error(\"\", slogs.Stack, string(debug.Stack()))\n\t\t\tprintLogo(color.Red)\n\t\t\tfmt.Printf(\"%s\", color.Colorize(\"Boom!! \", color.Red))\n\t\t\tfmt.Printf(\"%v.\\n\", err)\n\t\t}\n\t}()\n\n\tslog.SetDefault(slog.New(tint.NewHandler(logFile, &tint.Options{\n\t\tLevel:      parseLevel(*k9sFlags.LogLevel),\n\t\tTimeFormat: time.RFC3339,\n\t})))\n\n\tcfg, err := loadConfiguration()\n\tif err != nil {\n\t\tslog.Warn(\"Fail to load global/context configuration\", slogs.Error, err)\n\t}\n\tapp := view.NewApp(cfg)\n\tif app.Config.K9s.DefaultView != \"\" {\n\t\tapp.Config.SetActiveView(app.Config.K9s.DefaultView)\n\t}\n\n\tif err := app.Init(version, int(*k9sFlags.RefreshRate)); err != nil {\n\t\treturn err\n\t}\n\tif err := app.Run(); err != nil {\n\t\treturn err\n\t}\n\tif view.ExitStatus != \"\" {\n\t\treturn fmt.Errorf(\"view exit status %s\", view.ExitStatus)\n\t}\n\n\treturn nil\n}\n\nfunc loadConfiguration() (*config.Config, error) {\n\tslog.Info(\"🐶 K9s starting up...\")\n\n\tk8sCfg := client.NewConfig(k8sFlags)\n\tk9sCfg := config.NewConfig(k8sCfg)\n\tvar errs error\n\n\tconn, err := client.InitConnection(k8sCfg, slog.Default())\n\tif err != nil {\n\t\terrs = errors.Join(errs, err)\n\t}\n\tk9sCfg.SetConnection(conn)\n\n\tif err := k9sCfg.Load(config.AppConfigFile, false); err != nil {\n\t\terrs = errors.Join(errs, err)\n\t}\n\tk9sCfg.K9s.Override(k9sFlags)\n\tif err := k9sCfg.Refine(k8sFlags, k9sFlags, k8sCfg); err != nil {\n\t\tslog.Error(\"Fail to refine k9s config\", slogs.Error, err)\n\t\terrs = errors.Join(errs, err)\n\t}\n\n\t// Try to access server version if that fail. Connectivity issue?\n\tif !conn.CheckConnectivity() {\n\t\terrs = errors.Join(errs, fmt.Errorf(\"cannot connect to context: %s\", k9sCfg.K9s.ActiveContextName()))\n\t}\n\tif !conn.ConnectionOK() {\n\t\tslog.Warn(\"💣 Kubernetes connectivity toast!\")\n\t\terrs = errors.Join(errs, fmt.Errorf(\"k8s connection failed for context: %s\", k9sCfg.K9s.ActiveContextName()))\n\t} else {\n\t\tslog.Info(\"✅ Kubernetes connectivity OK\")\n\t}\n\n\tif err := k9sCfg.Save(false); err != nil {\n\t\tslog.Error(\"K9s config save failed\", slogs.Error, err)\n\t\terrs = errors.Join(errs, err)\n\t}\n\n\treturn k9sCfg, errs\n}\n\nfunc parseLevel(level string) slog.Level {\n\tswitch level {\n\tcase \"debug\":\n\t\treturn slog.LevelDebug\n\tcase \"warn\":\n\t\treturn slog.LevelWarn\n\tcase \"error\":\n\t\treturn slog.LevelError\n\tdefault:\n\t\treturn slog.LevelInfo\n\t}\n}\n\nfunc initK9sFlags() {\n\tk9sFlags = config.NewFlags()\n\trootCmd.Flags().Float32VarP(\n\t\tk9sFlags.RefreshRate,\n\t\t\"refresh\", \"r\",\n\t\tconfig.DefaultRefreshRate,\n\t\t\"Specify the default refresh rate as a float (sec)\",\n\t)\n\trootCmd.Flags().StringVarP(\n\t\tk9sFlags.LogLevel,\n\t\t\"logLevel\", \"l\",\n\t\tconfig.DefaultLogLevel,\n\t\t\"Specify a log level (error, warn, info, debug)\",\n\t)\n\trootCmd.Flags().StringVarP(\n\t\tk9sFlags.LogFile,\n\t\t\"logFile\", \"\",\n\t\tconfig.AppLogFile,\n\t\t\"Specify the log file\",\n\t)\n\trootCmd.Flags().BoolVar(\n\t\tk9sFlags.Headless,\n\t\t\"headless\",\n\t\tfalse,\n\t\t\"Turn K9s header off\",\n\t)\n\trootCmd.Flags().BoolVar(\n\t\tk9sFlags.Logoless,\n\t\t\"logoless\",\n\t\tfalse,\n\t\t\"Turn K9s logo off\",\n\t)\n\trootCmd.Flags().BoolVar(\n\t\tk9sFlags.Crumbsless,\n\t\t\"crumbsless\",\n\t\tfalse,\n\t\t\"Turn K9s crumbs off\",\n\t)\n\trootCmd.Flags().BoolVar(\n\t\tk9sFlags.Splashless,\n\t\t\"splashless\",\n\t\tfalse,\n\t\t\"Turn K9s splash screen off\",\n\t)\n\trootCmd.Flags().BoolVar(\n\t\tk9sFlags.Invert,\n\t\t\"invert\",\n\t\tfalse,\n\t\t\"Invert skin (dark to light, light to dark), preserving colors\",\n\t)\n\trootCmd.Flags().BoolVarP(\n\t\tk9sFlags.AllNamespaces,\n\t\t\"all-namespaces\", \"A\",\n\t\tfalse,\n\t\t\"Launch K9s in all namespaces\",\n\t)\n\trootCmd.Flags().StringVarP(\n\t\tk9sFlags.Command,\n\t\t\"command\", \"c\",\n\t\tconfig.DefaultCommand,\n\t\t\"Overrides the default resource to load when the application launches\",\n\t)\n\trootCmd.Flags().BoolVar(\n\t\tk9sFlags.ReadOnly,\n\t\t\"readonly\",\n\t\tfalse,\n\t\t\"Sets readOnly mode by overriding readOnly configuration setting\",\n\t)\n\trootCmd.Flags().BoolVar(\n\t\tk9sFlags.Write,\n\t\t\"write\",\n\t\tfalse,\n\t\t\"Sets write mode by overriding the readOnly configuration setting\",\n\t)\n\trootCmd.Flags().StringVar(\n\t\tk9sFlags.ScreenDumpDir,\n\t\t\"screen-dump-dir\",\n\t\t\"\",\n\t\t\"Sets a path to a dir for a screen dumps\",\n\t)\n\trootCmd.Flags()\n}\n\nfunc initK8sFlags() {\n\tk8sFlags = genericclioptions.NewConfigFlags(client.UsePersistentConfig)\n\n\trootCmd.Flags().StringVar(\n\t\tk8sFlags.KubeConfig,\n\t\t\"kubeconfig\",\n\t\t\"\",\n\t\t\"Path to the kubeconfig file to use for CLI requests\",\n\t)\n\n\trootCmd.Flags().StringVar(\n\t\tk8sFlags.Timeout,\n\t\t\"request-timeout\",\n\t\t\"\",\n\t\t\"The length of time to wait before giving up on a single server request\",\n\t)\n\n\trootCmd.Flags().StringVar(\n\t\tk8sFlags.Context,\n\t\t\"context\",\n\t\t\"\",\n\t\t\"The name of the kubeconfig context to use\",\n\t)\n\n\trootCmd.Flags().StringVar(\n\t\tk8sFlags.ClusterName,\n\t\t\"cluster\",\n\t\t\"\",\n\t\t\"The name of the kubeconfig cluster to use\",\n\t)\n\n\trootCmd.Flags().StringVar(\n\t\tk8sFlags.AuthInfoName,\n\t\t\"user\",\n\t\t\"\",\n\t\t\"The name of the kubeconfig user to use\",\n\t)\n\n\trootCmd.Flags().StringVarP(\n\t\tk8sFlags.Namespace,\n\t\t\"namespace\",\n\t\t\"n\",\n\t\t\"\",\n\t\t\"If present, the namespace scope for this CLI request\",\n\t)\n\n\tinitAsFlags()\n\tinitCertFlags()\n\tinitK8sFlagCompletion()\n}\n\nfunc initAsFlags() {\n\trootCmd.Flags().StringVar(\n\t\tk8sFlags.Impersonate,\n\t\t\"as\",\n\t\t\"\",\n\t\t\"Username to impersonate for the operation\",\n\t)\n\n\trootCmd.Flags().StringArrayVar(\n\t\tk8sFlags.ImpersonateGroup,\n\t\t\"as-group\",\n\t\t[]string{},\n\t\t\"Group to impersonate for the operation\",\n\t)\n}\n\nfunc initCertFlags() {\n\trootCmd.Flags().BoolVar(\n\t\tk8sFlags.Insecure,\n\t\t\"insecure-skip-tls-verify\",\n\t\tfalse,\n\t\t\"If true, the server's caCertFile will not be checked for validity\",\n\t)\n\n\trootCmd.Flags().StringVar(\n\t\tk8sFlags.CAFile,\n\t\t\"certificate-authority\",\n\t\t\"\",\n\t\t\"Path to a cert file for the certificate authority\",\n\t)\n\n\trootCmd.Flags().StringVar(\n\t\tk8sFlags.KeyFile,\n\t\t\"client-key\",\n\t\t\"\",\n\t\t\"Path to a client key file for TLS\",\n\t)\n\n\trootCmd.Flags().StringVar(\n\t\tk8sFlags.CertFile,\n\t\t\"client-certificate\",\n\t\t\"\",\n\t\t\"Path to a client certificate file for TLS\",\n\t)\n\n\trootCmd.Flags().StringVar(\n\t\tk8sFlags.BearerToken,\n\t\t\"token\",\n\t\t\"\",\n\t\t\"Bearer token for authentication to the API server\",\n\t)\n}\n\ntype (\n\tk8sPickerFn[T any] func(cfg *api.Config) map[string]T\n\tcompleteFn         func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective)\n)\n\nfunc initK8sFlagCompletion() {\n\t_ = rootCmd.RegisterFlagCompletionFunc(\"context\", k8sFlagCompletion(func(cfg *api.Config) map[string]*api.Context {\n\t\treturn cfg.Contexts\n\t}))\n\n\t_ = rootCmd.RegisterFlagCompletionFunc(\"cluster\", k8sFlagCompletion(func(cfg *api.Config) map[string]*api.Cluster {\n\t\treturn cfg.Clusters\n\t}))\n\n\t_ = rootCmd.RegisterFlagCompletionFunc(\"user\", k8sFlagCompletion(func(cfg *api.Config) map[string]*api.AuthInfo {\n\t\treturn cfg.AuthInfos\n\t}))\n\n\t_ = rootCmd.RegisterFlagCompletionFunc(\"namespace\", func(_ *cobra.Command, _ []string, s string) ([]string, cobra.ShellCompDirective) {\n\t\tconn := client.NewConfig(k8sFlags)\n\t\tif c, err := client.InitConnection(conn, slog.Default()); err == nil {\n\t\t\tif nss, err := c.ValidNamespaceNames(); err == nil {\n\t\t\t\treturn filterFlagCompletions(nss, s)\n\t\t\t}\n\t\t}\n\n\t\treturn nil, cobra.ShellCompDirectiveError\n\t})\n}\n\nfunc k8sFlagCompletion[T any](picker k8sPickerFn[T]) completeFn {\n\treturn func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {\n\t\tconn := client.NewConfig(k8sFlags)\n\t\tcfg, err := conn.RawConfig()\n\t\tif err != nil {\n\t\t\tslog.Error(\"K8s raw config getter failed\", slogs.Error, err)\n\t\t}\n\n\t\treturn filterFlagCompletions(picker(&cfg), toComplete)\n\t}\n}\n\nfunc filterFlagCompletions[T any](m map[string]T, s string) ([]string, cobra.ShellCompDirective) {\n\tcc := make([]string, 0, len(m))\n\tfor name := range m {\n\t\tif strings.HasPrefix(name, s) {\n\t\t\tcc = append(cc, name)\n\t\t}\n\t}\n\n\treturn cc, cobra.ShellCompDirectiveNoFileComp\n}\n"
  },
  {
    "path": "cmd/testdata/k9s.yaml",
    "content": "k9s:\n  refreshRate: 2\n  readOnly: false\n  logger:\n    tail: 200\n    buffer: 2000\n  currentContext: minikube\n  currentCluster: minikube\n  clusters:\n    minikube:\n      namespace:\n        active: kube-system\n        favorites:\n          - default\n          - kube-public\n          - istio-system\n          - all\n          - kube-system\n      view:\n        active: ctx\n    fred:\n      namespace:\n        active: default\n        favorites:\n          - default\n          - kube-public\n          - istio-system\n          - all\n          - kube-system\n      view:\n        active: po\n  screenDumpDir: /tmp\n"
  },
  {
    "path": "cmd/testdata/k9s1.yaml",
    "content": "k9s:\n  refreshRate: 10\n  namespace:\n    active: fred\n    favorites:\n      - blee\n      - duh\n      - crap"
  },
  {
    "path": "cmd/version.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage cmd\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal/color\"\n\t\"github.com/spf13/cobra\"\n)\n\nfunc versionCmd() *cobra.Command {\n\tvar short bool\n\n\tcommand := cobra.Command{\n\t\tUse:   \"version\",\n\t\tShort: \"Print version/build info\",\n\t\tLong:  \"Print version/build information\",\n\t\tRun: func(*cobra.Command, []string) {\n\t\t\tprintVersion(short)\n\t\t},\n\t}\n\n\tcommand.PersistentFlags().BoolVarP(&short, \"short\", \"s\", false, \"Prints K9s version info in short format\")\n\n\treturn &command\n}\n\nfunc printVersion(short bool) {\n\tconst fmat = \"%-20s %s\\n\"\n\tvar outputColor color.Paint\n\n\tif short {\n\t\toutputColor = -1\n\t} else {\n\t\toutputColor = color.Cyan\n\t\tprintLogo(outputColor)\n\t}\n\tprintTuple(fmat, \"Version\", version, outputColor)\n\tprintTuple(fmat, \"Commit\", commit, outputColor)\n\tprintTuple(fmat, \"Date\", date, outputColor)\n}\n\nfunc printTuple(fmat, section, value string, outputColor color.Paint) {\n\tif outputColor != -1 {\n\t\t_, _ = fmt.Fprintf(out, fmat, color.Colorize(section+\":\", outputColor), value)\n\t\treturn\n\t}\n\t_, _ = fmt.Fprintf(out, fmat, section, value)\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/derailed/k9s\n\ngo 1.25.1\n\nrequire (\n\tgithub.com/adrg/xdg v0.5.3\n\tgithub.com/anchore/clio v0.0.0-20250715152405-a0fa658e5084\n\tgithub.com/anchore/grype v0.109.0\n\tgithub.com/anchore/syft v1.42.1\n\tgithub.com/atotto/clipboard v0.1.4\n\tgithub.com/cenkalti/backoff/v4 v4.3.0\n\tgithub.com/derailed/tcell/v2 v2.3.1-rc.4\n\tgithub.com/derailed/tview v0.8.5\n\tgithub.com/fatih/color v1.18.0\n\tgithub.com/fsnotify/fsnotify v1.9.0\n\tgithub.com/fvbommel/sortorder v1.1.0\n\tgithub.com/go-errors/errors v1.5.1\n\tgithub.com/itchyny/gojq v0.12.18\n\tgithub.com/karrick/godirwalk v1.17.0\n\tgithub.com/lmittmann/tint v1.1.3\n\tgithub.com/lucasb-eyer/go-colorful v1.3.0\n\tgithub.com/mattn/go-colorable v0.1.14\n\tgithub.com/mattn/go-runewidth v0.0.19\n\tgithub.com/olekukonko/tablewriter v1.1.3\n\tgithub.com/petergtz/pegomock v2.9.0+incompatible\n\tgithub.com/rakyll/hey v0.1.5\n\tgithub.com/sahilm/fuzzy v0.1.1\n\tgithub.com/spf13/cobra v1.10.2\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/xeipuuv/gojsonschema v1.2.0\n\tgolang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546\n\tgolang.org/x/text v0.34.0\n\tgopkg.in/yaml.v3 v3.0.1\n\thelm.sh/helm/v3 v3.20.0\n\tk8s.io/api v0.35.2\n\tk8s.io/apiextensions-apiserver v0.35.2\n\tk8s.io/apimachinery v0.35.2\n\tk8s.io/cli-runtime v0.35.1\n\tk8s.io/client-go v0.35.2\n\tk8s.io/klog/v2 v2.140.0\n\tk8s.io/kubectl v0.35.0\n\tk8s.io/metrics v0.35.2\n\tsigs.k8s.io/yaml v1.6.0\n)\n\nrequire (\n\tcel.dev/expr v0.24.0 // indirect\n\tcloud.google.com/go v0.123.0 // indirect\n\tcloud.google.com/go/auth v0.17.0 // indirect\n\tcloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect\n\tcloud.google.com/go/compute/metadata v0.9.0 // indirect\n\tcloud.google.com/go/iam v1.5.3 // indirect\n\tcloud.google.com/go/monitoring v1.24.2 // indirect\n\tcloud.google.com/go/storage v1.58.0 // indirect\n\tcyphar.com/go-pathrs v0.2.1 // indirect\n\tdario.cat/mergo v1.0.2 // indirect\n\tgithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect\n\tgithub.com/BurntSushi/toml v1.6.0 // indirect\n\tgithub.com/CycloneDX/cyclonedx-go v0.10.0 // indirect\n\tgithub.com/DataDog/zstd v1.5.7 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect\n\tgithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect\n\tgithub.com/Intevation/gval v1.3.0 // indirect\n\tgithub.com/Intevation/jsonpath v0.2.1 // indirect\n\tgithub.com/MakeNowJust/heredoc v1.0.0 // indirect\n\tgithub.com/Masterminds/goutils v1.1.1 // indirect\n\tgithub.com/Masterminds/semver/v3 v3.4.0 // indirect\n\tgithub.com/Masterminds/sprig/v3 v3.3.0 // indirect\n\tgithub.com/Masterminds/squirrel v1.5.4 // indirect\n\tgithub.com/Microsoft/go-winio v0.6.2 // indirect\n\tgithub.com/Microsoft/hcsshim v0.14.0-rc.1 // indirect\n\tgithub.com/OneOfOne/xxhash v1.2.8 // indirect\n\tgithub.com/ProtonMail/go-crypto v1.3.0 // indirect\n\tgithub.com/STARRY-S/zip v0.2.3 // indirect\n\tgithub.com/acobaugh/osrelease v0.1.0 // indirect\n\tgithub.com/agext/levenshtein v1.2.3 // indirect\n\tgithub.com/anchore/fangs v0.0.0-20250716230140-94c22408c232 // indirect\n\tgithub.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c // indirect\n\tgithub.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d // indirect\n\tgithub.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 // indirect\n\tgithub.com/anchore/go-lzo v0.1.0 // indirect\n\tgithub.com/anchore/go-macholibre v0.0.0-20250320151634-807da7ad2331 // indirect\n\tgithub.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec // indirect\n\tgithub.com/anchore/go-struct-converter v0.1.0 // indirect\n\tgithub.com/anchore/go-sync v0.0.0-20250714163430-add63db73ad1 // indirect\n\tgithub.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 // indirect\n\tgithub.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 // indirect\n\tgithub.com/anchore/stereoscope v0.1.20 // indirect\n\tgithub.com/andybalholm/brotli v1.2.0 // indirect\n\tgithub.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect\n\tgithub.com/aquasecurity/go-pep440-version v0.0.1 // indirect\n\tgithub.com/aquasecurity/go-version v0.0.1 // indirect\n\tgithub.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de // indirect\n\tgithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect\n\tgithub.com/aws/aws-sdk-go-v2 v1.41.0 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/config v1.32.6 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/credentials v1.19.6 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sso v1.30.8 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect\n\tgithub.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect\n\tgithub.com/aws/smithy-go v1.24.0 // indirect\n\tgithub.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect\n\tgithub.com/becheran/wildmatch-go v1.0.0 // indirect\n\tgithub.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect\n\tgithub.com/bitnami/go-version v0.0.0-20250505154626-452e8c5ee607 // indirect\n\tgithub.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect\n\tgithub.com/blang/semver/v4 v4.0.0 // indirect\n\tgithub.com/bmatcuk/doublestar/v2 v2.0.4 // indirect\n\tgithub.com/bmatcuk/doublestar/v4 v4.10.0 // indirect\n\tgithub.com/bodgit/plumbing v1.3.0 // indirect\n\tgithub.com/bodgit/sevenzip v1.6.1 // indirect\n\tgithub.com/bodgit/windows v1.0.1 // indirect\n\tgithub.com/cespare/xxhash/v2 v2.3.0 // indirect\n\tgithub.com/chai2010/gettext-go v1.0.2 // indirect\n\tgithub.com/charmbracelet/colorprofile v0.4.1 // indirect\n\tgithub.com/charmbracelet/lipgloss v1.1.0 // indirect\n\tgithub.com/charmbracelet/x/ansi v0.11.5 // indirect\n\tgithub.com/charmbracelet/x/cellbuf v0.0.15 // indirect\n\tgithub.com/charmbracelet/x/term v0.2.2 // indirect\n\tgithub.com/clipperhouse/displaywidth v0.9.0 // indirect\n\tgithub.com/clipperhouse/stringish v0.1.1 // indirect\n\tgithub.com/clipperhouse/uax29/v2 v2.5.0 // indirect\n\tgithub.com/cloudflare/circl v1.6.3 // indirect\n\tgithub.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect\n\tgithub.com/containerd/cgroups/v3 v3.1.2 // indirect\n\tgithub.com/containerd/containerd v1.7.30 // indirect\n\tgithub.com/containerd/containerd/api v1.10.0 // indirect\n\tgithub.com/containerd/containerd/v2 v2.2.1 // indirect\n\tgithub.com/containerd/continuity v0.4.5 // indirect\n\tgithub.com/containerd/errdefs v1.0.0 // indirect\n\tgithub.com/containerd/errdefs/pkg v0.3.0 // indirect\n\tgithub.com/containerd/fifo v1.1.0 // indirect\n\tgithub.com/containerd/log v0.1.0 // indirect\n\tgithub.com/containerd/platforms v1.0.0-rc.2 // indirect\n\tgithub.com/containerd/plugin v1.0.0 // indirect\n\tgithub.com/containerd/stargz-snapshotter/estargz v0.18.1 // indirect\n\tgithub.com/containerd/ttrpc v1.2.7 // indirect\n\tgithub.com/containerd/typeurl/v2 v2.2.3 // indirect\n\tgithub.com/creack/pty v1.1.20 // indirect\n\tgithub.com/cyphar/filepath-securejoin v0.6.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/deitch/magic v0.0.0-20240306090643-c67ab88f10cb // indirect\n\tgithub.com/diskfs/go-diskfs v1.7.0 // indirect\n\tgithub.com/distribution/reference v0.6.0 // indirect\n\tgithub.com/docker/cli v29.2.0+incompatible // indirect\n\tgithub.com/docker/distribution v2.8.3+incompatible // indirect\n\tgithub.com/docker/docker v28.5.2+incompatible // indirect\n\tgithub.com/docker/docker-credential-helpers v0.9.4 // indirect\n\tgithub.com/docker/go-connections v0.6.0 // indirect\n\tgithub.com/docker/go-events v0.0.0-20250114142523-c867878c5e32 // indirect\n\tgithub.com/docker/go-units v0.5.0 // indirect\n\tgithub.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect\n\tgithub.com/dustin/go-humanize v1.0.1 // indirect\n\tgithub.com/elliotchance/phpserialize v1.4.0 // indirect\n\tgithub.com/emicklei/go-restful/v3 v3.13.0 // indirect\n\tgithub.com/emirpasic/gods v1.18.1 // indirect\n\tgithub.com/envoyproxy/go-control-plane/envoy v1.32.4 // indirect\n\tgithub.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect\n\tgithub.com/evanphx/json-patch v5.9.11+incompatible // indirect\n\tgithub.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect\n\tgithub.com/facebookincubator/nvdtools v0.1.5 // indirect\n\tgithub.com/fatih/camelcase v1.0.0 // indirect\n\tgithub.com/felixge/fgprof v0.9.5 // indirect\n\tgithub.com/felixge/httpsnoop v1.0.4 // indirect\n\tgithub.com/fxamacker/cbor/v2 v2.9.0 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.13 // indirect\n\tgithub.com/gdamore/encoding v1.0.1 // indirect\n\tgithub.com/github/go-spdx/v2 v2.3.6 // indirect\n\tgithub.com/glebarez/go-sqlite v1.22.0 // indirect\n\tgithub.com/glebarez/sqlite v1.11.0 // indirect\n\tgithub.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect\n\tgithub.com/go-git/go-billy/v5 v5.7.0 // indirect\n\tgithub.com/go-git/go-git/v5 v5.16.5 // indirect\n\tgithub.com/go-gorp/gorp/v3 v3.1.0 // indirect\n\tgithub.com/go-jose/go-jose/v4 v4.1.2 // indirect\n\tgithub.com/go-logr/logr v1.4.3 // indirect\n\tgithub.com/go-logr/stdr v1.2.2 // indirect\n\tgithub.com/go-openapi/jsonpointer v0.21.0 // indirect\n\tgithub.com/go-openapi/jsonreference v0.20.2 // indirect\n\tgithub.com/go-openapi/swag v0.23.0 // indirect\n\tgithub.com/go-restruct/restruct v1.2.0-alpha // indirect\n\tgithub.com/go-viper/mapstructure/v2 v2.5.0 // indirect\n\tgithub.com/gobwas/glob v0.2.3 // indirect\n\tgithub.com/goccy/go-yaml v1.19.2 // indirect\n\tgithub.com/gocsaf/csaf/v3 v3.5.1 // indirect\n\tgithub.com/gogo/protobuf v1.3.2 // indirect\n\tgithub.com/gohugoio/hashstructure v0.6.0 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect\n\tgithub.com/google/btree v1.1.3 // indirect\n\tgithub.com/google/gnostic-models v0.7.0 // indirect\n\tgithub.com/google/go-cmp v0.7.0 // indirect\n\tgithub.com/google/go-containerregistry v0.20.7 // indirect\n\tgithub.com/google/licensecheck v0.3.1 // indirect\n\tgithub.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 // indirect\n\tgithub.com/google/s2a-go v0.1.9 // indirect\n\tgithub.com/google/uuid v1.6.0 // indirect\n\tgithub.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect\n\tgithub.com/googleapis/gax-go/v2 v2.15.0 // indirect\n\tgithub.com/gookit/color v1.6.0 // indirect\n\tgithub.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect\n\tgithub.com/gosuri/uitable v0.0.4 // indirect\n\tgithub.com/gpustack/gguf-parser-go v0.23.1 // indirect\n\tgithub.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect\n\tgithub.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b // indirect\n\tgithub.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 // indirect\n\tgithub.com/hashicorp/errwrap v1.1.0 // indirect\n\tgithub.com/hashicorp/go-cleanhttp v0.5.2 // indirect\n\tgithub.com/hashicorp/go-getter v1.8.4 // indirect\n\tgithub.com/hashicorp/go-multierror v1.1.1 // indirect\n\tgithub.com/hashicorp/go-version v1.8.0 // indirect\n\tgithub.com/hashicorp/golang-lru/v2 v2.0.7 // indirect\n\tgithub.com/hashicorp/hcl/v2 v2.24.0 // indirect\n\tgithub.com/henvic/httpretty v0.1.4 // indirect\n\tgithub.com/huandu/xstrings v1.5.0 // indirect\n\tgithub.com/iancoleman/strcase v0.3.0 // indirect\n\tgithub.com/inconshreveable/mousetrap v1.1.0 // indirect\n\tgithub.com/itchyny/timefmt-go v0.1.7 // indirect\n\tgithub.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect\n\tgithub.com/jinzhu/copier v0.4.0 // indirect\n\tgithub.com/jinzhu/inflection v1.0.0 // indirect\n\tgithub.com/jinzhu/now v1.1.5 // indirect\n\tgithub.com/jmoiron/sqlx v1.4.0 // indirect\n\tgithub.com/josharian/intern v1.0.0 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 // indirect\n\tgithub.com/kevinburke/ssh_config v1.2.0 // indirect\n\tgithub.com/klauspost/compress v1.18.4 // indirect\n\tgithub.com/klauspost/pgzip v1.2.6 // indirect\n\tgithub.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f // indirect\n\tgithub.com/knqyf263/go-deb-version v0.0.0-20241115132648-6f4aee6ccd23 // indirect\n\tgithub.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect\n\tgithub.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect\n\tgithub.com/lib/pq v1.10.9 // indirect\n\tgithub.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect\n\tgithub.com/mailru/easyjson v0.9.0 // indirect\n\tgithub.com/masahiro331/go-mvn-version v0.0.0-20250131095131-f4974fa13b8a // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect\n\tgithub.com/mholt/archives v0.1.5 // indirect\n\tgithub.com/mikelolasagasti/xz v1.0.1 // indirect\n\tgithub.com/minio/minlz v1.0.1 // indirect\n\tgithub.com/mitchellh/copystructure v1.2.0 // indirect\n\tgithub.com/mitchellh/go-homedir v1.1.0 // indirect\n\tgithub.com/mitchellh/go-wordwrap v1.0.1 // indirect\n\tgithub.com/mitchellh/reflectwalk v1.0.2 // indirect\n\tgithub.com/moby/docker-image-spec v1.3.1 // indirect\n\tgithub.com/moby/locker v1.0.1 // indirect\n\tgithub.com/moby/spdystream v0.5.0 // indirect\n\tgithub.com/moby/sys/mountinfo v0.7.2 // indirect\n\tgithub.com/moby/sys/sequential v0.6.0 // indirect\n\tgithub.com/moby/sys/signal v0.7.1 // indirect\n\tgithub.com/moby/sys/user v0.4.0 // indirect\n\tgithub.com/moby/sys/userns v0.1.0 // indirect\n\tgithub.com/moby/term v0.5.2 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect\n\tgithub.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect\n\tgithub.com/muesli/termenv v0.16.0 // indirect\n\tgithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect\n\tgithub.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect\n\tgithub.com/ncruces/go-strftime v1.0.0 // indirect\n\tgithub.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1 // indirect\n\tgithub.com/nwaples/rardecode/v2 v2.2.0 // indirect\n\tgithub.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect\n\tgithub.com/olekukonko/errors v1.1.0 // indirect\n\tgithub.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 // indirect\n\tgithub.com/onsi/ginkgo v1.16.5 // indirect\n\tgithub.com/onsi/gomega v1.38.2 // indirect\n\tgithub.com/opencontainers/go-digest v1.0.0 // indirect\n\tgithub.com/opencontainers/image-spec v1.1.1 // indirect\n\tgithub.com/opencontainers/runtime-spec v1.3.0 // indirect\n\tgithub.com/opencontainers/selinux v1.13.1 // indirect\n\tgithub.com/openvex/go-vex v0.2.7 // indirect\n\tgithub.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554 // indirect\n\tgithub.com/package-url/packageurl-go v0.1.3 // indirect\n\tgithub.com/pandatix/go-cvss v0.6.2 // indirect\n\tgithub.com/pborman/indent v1.2.1 // indirect\n\tgithub.com/pelletier/go-toml v1.9.5 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/peterbourgon/diskv v2.0.1+incompatible // indirect\n\tgithub.com/pierrec/lz4/v4 v4.1.22 // indirect\n\tgithub.com/pjbgf/sha1cd v0.4.0 // indirect\n\tgithub.com/pkg/errors v0.9.1 // indirect\n\tgithub.com/pkg/profile v1.7.0 // indirect\n\tgithub.com/pkg/xattr v0.4.12 // indirect\n\tgithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect\n\tgithub.com/rivo/uniseg v0.4.7 // indirect\n\tgithub.com/rubenv/sql-migrate v1.8.1 // indirect\n\tgithub.com/russross/blackfriday/v2 v2.1.0 // indirect\n\tgithub.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c // indirect\n\tgithub.com/sagikazarmark/locafero v0.11.0 // indirect\n\tgithub.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect\n\tgithub.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect\n\tgithub.com/sassoftware/go-rpmutils v0.4.0 // indirect\n\tgithub.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e // indirect\n\tgithub.com/sergi/go-diff v1.4.0 // indirect\n\tgithub.com/shopspring/decimal v1.4.0 // indirect\n\tgithub.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect\n\tgithub.com/skeema/knownhosts v1.3.1 // indirect\n\tgithub.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d // indirect\n\tgithub.com/sorairolake/lzip-go v0.3.8 // indirect\n\tgithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect\n\tgithub.com/spdx/gordf v0.0.0-20250128162952-000978ccd6fb // indirect\n\tgithub.com/spdx/tools-golang v0.5.7 // indirect\n\tgithub.com/spf13/afero v1.15.0 // indirect\n\tgithub.com/spf13/cast v1.10.0 // indirect\n\tgithub.com/spf13/pflag v1.0.10 // indirect\n\tgithub.com/spf13/viper v1.21.0 // indirect\n\tgithub.com/spiffe/go-spiffe/v2 v2.5.0 // indirect\n\tgithub.com/subosito/gotenv v1.6.0 // indirect\n\tgithub.com/sylabs/sif/v2 v2.22.0 // indirect\n\tgithub.com/sylabs/squashfs v1.0.6 // indirect\n\tgithub.com/therootcompany/xz v1.0.1 // indirect\n\tgithub.com/ulikunitz/xz v0.5.15 // indirect\n\tgithub.com/vbatts/go-mtree v0.7.0 // indirect\n\tgithub.com/vbatts/tar-split v0.12.2 // indirect\n\tgithub.com/vifraa/gopom v1.0.0 // indirect\n\tgithub.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 // indirect\n\tgithub.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b // indirect\n\tgithub.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 // indirect\n\tgithub.com/x448/float16 v0.8.4 // indirect\n\tgithub.com/xanzy/ssh-agent v0.3.3 // indirect\n\tgithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect\n\tgithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect\n\tgithub.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect\n\tgithub.com/xlab/treeprint v1.2.0 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgithub.com/zclconf/go-cty v1.16.3 // indirect\n\tgithub.com/zeebo/errs v1.4.0 // indirect\n\tgo.etcd.io/bbolt v1.4.3 // indirect\n\tgo.opencensus.io v0.24.0 // indirect\n\tgo.opentelemetry.io/auto/sdk v1.2.1 // indirect\n\tgo.opentelemetry.io/contrib/detectors/gcp v1.37.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect\n\tgo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 // indirect\n\tgo.opentelemetry.io/otel v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/metric v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/sdk v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect\n\tgo.opentelemetry.io/otel/trace v1.40.0 // indirect\n\tgo.yaml.in/yaml/v2 v2.4.3 // indirect\n\tgo.yaml.in/yaml/v3 v3.0.4 // indirect\n\tgo4.org v0.0.0-20230225012048-214862532bf5 // indirect\n\tgolang.org/x/crypto v0.47.0 // indirect\n\tgolang.org/x/mod v0.32.0 // indirect\n\tgolang.org/x/net v0.49.0 // indirect\n\tgolang.org/x/oauth2 v0.33.0 // indirect\n\tgolang.org/x/sync v0.19.0 // indirect\n\tgolang.org/x/sys v0.40.0 // indirect\n\tgolang.org/x/term v0.39.0 // indirect\n\tgolang.org/x/time v0.14.0 // indirect\n\tgolang.org/x/tools v0.41.0 // indirect\n\tgolang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect\n\tgonum.org/v1/gonum v0.16.0 // indirect\n\tgoogle.golang.org/api v0.256.0 // indirect\n\tgoogle.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 // indirect\n\tgoogle.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba // indirect\n\tgoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba // indirect\n\tgoogle.golang.org/grpc v1.76.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.10 // indirect\n\tgopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect\n\tgopkg.in/inf.v0 v0.9.1 // indirect\n\tgopkg.in/warnings.v0 v0.1.2 // indirect\n\tgorm.io/gorm v1.31.1 // indirect\n\tgotest.tools/v3 v3.4.0 // indirect\n\tk8s.io/apiserver v0.35.2 // indirect\n\tk8s.io/component-base v0.35.2 // indirect\n\tk8s.io/component-helpers v0.35.0 // indirect\n\tk8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect\n\tk8s.io/utils v0.0.0-20251002143259-bc988d571ff4 // indirect\n\tmodernc.org/libc v1.67.6 // indirect\n\tmodernc.org/mathutil v1.7.1 // indirect\n\tmodernc.org/memory v1.11.0 // indirect\n\tmodernc.org/sqlite v1.44.3 // indirect\n\toras.land/oras-go/v2 v2.6.0 // indirect\n\tsigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect\n\tsigs.k8s.io/kustomize/api v0.20.1 // indirect\n\tsigs.k8s.io/kustomize/kyaml v0.20.1 // indirect\n\tsigs.k8s.io/randfill v1.0.0 // indirect\n\tsigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=\ncel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=\ncloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=\ncloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=\ncloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=\ncloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=\ncloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=\ncloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=\ncloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=\ncloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=\ncloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=\ncloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=\ncloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=\ncloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=\ncloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=\ncloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=\ncloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=\ncloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=\ncloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=\ncloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=\ncloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=\ncloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY=\ncloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM=\ncloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY=\ncloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ=\ncloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI=\ncloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=\ncloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=\ncloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM=\ncloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=\ncloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=\ncloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=\ncloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=\ncloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ=\ncloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=\ncloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=\ncloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=\ncloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=\ncloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=\ncloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=\ncloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=\ncloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=\ncloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=\ncloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=\ncloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=\ncloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=\ncloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=\ncloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc=\ncloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU=\ncloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc=\ncloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA=\ncloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E=\ncloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY=\ncloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM=\ncloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U=\ncloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=\ncloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=\ncloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=\ncloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=\ncloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=\ncloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=\ncloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=\ncloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=\ncloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=\ncloud.google.com/go/storage v1.58.0 h1:PflFXlmFJjG/nBeR9B7pKddLQWaFaRWx4uUi/LyNxxo=\ncloud.google.com/go/storage v1.58.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI=\ncloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4=\ncloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI=\ncyphar.com/go-pathrs v0.2.1 h1:9nx1vOgwVvX1mNBWDu93+vaceedpbsDqo+XuBGL40b8=\ncyphar.com/go-pathrs v0.2.1/go.mod h1:y8f1EMG7r+hCuFf/rXsKqMJrJAUoADZGNh5/vZPKcGc=\ndario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=\ndario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=\ndmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=\nfilippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=\nfilippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=\ngithub.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=\ngithub.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=\ngithub.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=\ngithub.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=\ngithub.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=\ngithub.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=\ngithub.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=\ngithub.com/CycloneDX/cyclonedx-go v0.10.0 h1:7xyklU7YD+CUyGzSFIARG18NYLsKVn4QFg04qSsu+7Y=\ngithub.com/CycloneDX/cyclonedx-go v0.10.0/go.mod h1:vUvbCXQsEm48OI6oOlanxstwNByXjCZ2wuleUlwGEO8=\ngithub.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=\ngithub.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=\ngithub.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=\ngithub.com/DataDog/zstd v1.5.7 h1:ybO8RBeh29qrxIhCA9E8gKY6xfONU9T6G6aP9DTKfLE=\ngithub.com/DataDog/zstd v1.5.7/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 h1:UQUsRi8WTzhZntp5313l+CHIAT95ojUI2lpP/ExlZa4=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI=\ngithub.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=\ngithub.com/Intevation/gval v1.3.0 h1:+Ze5sft5MmGbZrHj06NVUbcxCb67l9RaPTLMNr37mjw=\ngithub.com/Intevation/gval v1.3.0/go.mod h1:xmGyGpP5be12EL0P12h+dqiYG8qn2j3PJxIgkoOHO5o=\ngithub.com/Intevation/jsonpath v0.2.1 h1:rINNQJ0Pts5XTFEG+zamtdL7l9uuE1z0FBA+r55Sw+A=\ngithub.com/Intevation/jsonpath v0.2.1/go.mod h1:WnZ8weMmwAx/fAO3SutjYFU+v7DFreNYnibV7CiaYIw=\ngithub.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=\ngithub.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=\ngithub.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=\ngithub.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=\ngithub.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=\ngithub.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=\ngithub.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=\ngithub.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=\ngithub.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM=\ngithub.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10=\ngithub.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/Microsoft/hcsshim v0.14.0-rc.1 h1:qAPXKwGOkVn8LlqgBN8GS0bxZ83hOJpcjxzmlQKxKsQ=\ngithub.com/Microsoft/hcsshim v0.14.0-rc.1/go.mod h1:hTKFGbnDtQb1wHiOWv4v0eN+7boSWAHyK/tNAaYZL0c=\ngithub.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=\ngithub.com/OneOfOne/xxhash v1.2.8 h1:31czK/TI9sNkxIKfaUfGlU47BAxQ0ztGgd9vPyqimf8=\ngithub.com/OneOfOne/xxhash v1.2.8/go.mod h1:eZbhyaAYD41SGSSsnmcpxVoRiQ/MPUTjUdIIOT9Um7Q=\ngithub.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=\ngithub.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=\ngithub.com/STARRY-S/zip v0.2.3 h1:luE4dMvRPDOWQdeDdUxUoZkzUIpTccdKdhHHsQJ1fm4=\ngithub.com/STARRY-S/zip v0.2.3/go.mod h1:lqJ9JdeRipyOQJrYSOtpNAiaesFO6zVDsE8GIGFaoSk=\ngithub.com/acobaugh/osrelease v0.1.0 h1:Yb59HQDGGNhCj4suHaFQQfBps5wyoKLSSX/J/+UifRE=\ngithub.com/acobaugh/osrelease v0.1.0/go.mod h1:4bFEs0MtgHNHBrmHCt67gNisnabCRAlzdVasCEGHTWY=\ngithub.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=\ngithub.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=\ngithub.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=\ngithub.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=\ngithub.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=\ngithub.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=\ngithub.com/anchore/clio v0.0.0-20250715152405-a0fa658e5084 h1:7DUAXEdAxoANPlDgxYiaSRKnWnTygvdrrWhnmvEjNLg=\ngithub.com/anchore/clio v0.0.0-20250715152405-a0fa658e5084/go.mod h1:42dWox8z4//b898OIELsQnSdYq9q1aCXkwp5fKF+BEU=\ngithub.com/anchore/fangs v0.0.0-20250716230140-94c22408c232 h1:aVC6r9h5wGNh8BYTW3CXxOdPoZzY/bBRWne1NvSTlO8=\ngithub.com/anchore/fangs v0.0.0-20250716230140-94c22408c232/go.mod h1:Zees1AEKNpXIRgdVAMYWITncarLFiPOtEQ7rl45V/h0=\ngithub.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c h1:eoJXyC0n7DZ4YvySG/ETdYkTar2Due7eH+UmLK6FbrA=\ngithub.com/anchore/go-collections v0.0.0-20251016125210-a3c352120e8c/go.mod h1:1aiktV46ATCkuVg0O573ZrH56BUawTECPETbZyBcqT8=\ngithub.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d h1:gT69osH9AsdpOfqxbRwtxcNnSZ1zg4aKy2BevO3ZBdc=\ngithub.com/anchore/go-homedir v0.0.0-20250319154043-c29668562e4d/go.mod h1:PhSnuFYknwPZkOWKB1jXBNToChBA+l0FjwOxtViIc50=\ngithub.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722 h1:2SqmFgE7h+Ql4VyBzhjLkRF/3gDrcpUBj8LjvvO6OOM=\ngithub.com/anchore/go-logger v0.0.0-20250318195838-07ae343dd722/go.mod h1:oFuE8YuTCM+spgMXhePGzk3asS94yO9biUfDzVTFqNw=\ngithub.com/anchore/go-lzo v0.1.0 h1:NgAacnzqPeGH49Ky19QKLBZEuFRqtTG9cdaucc3Vncs=\ngithub.com/anchore/go-lzo v0.1.0/go.mod h1:3kLx0bve2oN1iDwgM1U5zGku1Tfbdb0No5qp1eL1fIk=\ngithub.com/anchore/go-macholibre v0.0.0-20250320151634-807da7ad2331 h1:fWPHXkH3FQGVCyPkFMqNvMjQvdNMfkylBTsDqZC4lE4=\ngithub.com/anchore/go-macholibre v0.0.0-20250320151634-807da7ad2331/go.mod h1:DYvTRnWrlJ//6YOR83SiewmJiNFDEMRaOTnrzgco9FA=\ngithub.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec h1:SjjPMOXTzpuU1ZME4XeoHyek+dry3/C7I8gzaCo02eg=\ngithub.com/anchore/go-rpmdb v0.0.0-20250516171929-f77691e1faec/go.mod h1:eQVa6QFGzKy0qMcnW2pez0XBczvgwSjw9vA23qifEyU=\ngithub.com/anchore/go-struct-converter v0.1.0 h1:2rDRssAl6mgKBSLNiVCMADgZRhoqtw9dedlWa0OhD30=\ngithub.com/anchore/go-struct-converter v0.1.0/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA=\ngithub.com/anchore/go-sync v0.0.0-20250714163430-add63db73ad1 h1:UK1SWZf2xD5jq8QVeDdpt6wW31cO3RckBvPmGlDrTkg=\ngithub.com/anchore/go-sync v0.0.0-20250714163430-add63db73ad1/go.mod h1:hd0Ol9qFM8tRDdF50a+DpZEoB0HFNaEnCp/BSVyBRlg=\ngithub.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0vW0nnNKJfJieyH/TZ9UYAnTZs5/gHTdAe8=\ngithub.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04/go.mod h1:6dK64g27Qi1qGQZ67gFmBFvEHScy0/C8qhQhNe5B5pQ=\ngithub.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 h1:rmZG77uXgE+o2gozGEBoUMpX27lsku+xrMwlmBZJtbg=\ngithub.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=\ngithub.com/anchore/grype v0.109.0 h1:CRknQY4Mbpxh57E1O7Q/vSul9ltgzHqeSzza6alErME=\ngithub.com/anchore/grype v0.109.0/go.mod h1:4YbSZ8quer0N1CFJ8LQPFfcHIerXo6ccBTLlhd6IYGY=\ngithub.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 h1:ZyRCmiEjnoGJZ1+Ah0ZZ/mKKqNhGcUZBl0s7PTTDzvY=\ngithub.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115/go.mod h1:KoYIv7tdP5+CC9VGkeZV4/vGCKsY55VvoG+5dadg4YI=\ngithub.com/anchore/stereoscope v0.1.20 h1:32720yZ/YtvzF5tvsoRL/ibdAJzOdIaR444fDXW4arQ=\ngithub.com/anchore/stereoscope v0.1.20/go.mod h1:6Ef0xQAuN2Ito7eV9A9pYjD1x/0cX5fy56MwgEGyrB4=\ngithub.com/anchore/syft v1.42.1 h1:aZvkRXzclT2VrQUfu6tsyiixqusGJk9DeoOJktcQBrU=\ngithub.com/anchore/syft v1.42.1/go.mod h1:uo2xEPi6gyc/qabZFv0Oni6W2pL0gE7sshAyZJCnHNg=\ngithub.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=\ngithub.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=\ngithub.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=\ngithub.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=\ngithub.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=\ngithub.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=\ngithub.com/aquasecurity/go-pep440-version v0.0.1 h1:8VKKQtH2aV61+0hovZS3T//rUF+6GDn18paFTVS0h0M=\ngithub.com/aquasecurity/go-pep440-version v0.0.1/go.mod h1:3naPe+Bp6wi3n4l5iBFCZgS0JG8vY6FT0H4NGhFJ+i4=\ngithub.com/aquasecurity/go-version v0.0.1 h1:4cNl516agK0TCn5F7mmYN+xVs1E3S45LkgZk3cbaW2E=\ngithub.com/aquasecurity/go-version v0.0.1/go.mod h1:s1UU6/v2hctXcOa3OLwfj5d9yoXHa3ahf+ipSwEvGT0=\ngithub.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de h1:FxWPpzIjnTlhPwqqXc4/vE0f7GvRjuAsbW+HOIe8KnA=\ngithub.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de/go.mod h1:DCaWoUhZrYW9p1lxo/cm8EmUOOzAPSEZNGF2DK1dJgw=\ngithub.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=\ngithub.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=\ngithub.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc=\ngithub.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=\ngithub.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=\ngithub.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=\ngithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=\ngithub.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=\ngithub.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=\ngithub.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=\ngithub.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=\ngithub.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=\ngithub.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.6 h1:hFLBGUKjmLAekvi1evLi5hVvFQtSo3GYwi+Bx4lpJf8=\ngithub.com/aws/aws-sdk-go-v2/config v1.32.6/go.mod h1:lcUL/gcd8WyjCrMnxez5OXkO3/rwcNmvfno62tnXNcI=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.6 h1:F9vWao2TwjV2MyiyVS+duza0NIRtAslgLUM0vTA1ZaE=\ngithub.com/aws/aws-sdk-go-v2/credentials v1.19.6/go.mod h1:SgHzKjEVsdQr6Opor0ihgWtkWdfRAIwxYzSJ8O85VHY=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k=\ngithub.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc=\ngithub.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U=\ngithub.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk=\ngithub.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic=\ngithub.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E=\ngithub.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso=\ngithub.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI=\ngithub.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU=\ngithub.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.95.0 h1:MIWra+MSq53CFaXXAywB2qg9YvVZifkk6vEGl/1Qor0=\ngithub.com/aws/aws-sdk-go-v2/service/s3 v1.95.0/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ=\ngithub.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.8 h1:aM/Q24rIlS3bRAhTyFurowU8A0SMyGDtEOY/l/s/1Uw=\ngithub.com/aws/aws-sdk-go-v2/service/sso v1.30.8/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE=\ngithub.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX1s+lFTg4+4DOy70=\ngithub.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=\ngithub.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=\ngithub.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=\ngithub.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=\ngithub.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA=\ngithub.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4=\ngithub.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=\ngithub.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=\ngithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=\ngithub.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=\ngithub.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=\ngithub.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=\ngithub.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=\ngithub.com/bitnami/go-version v0.0.0-20250505154626-452e8c5ee607 h1:lBg3tHGquFySSblLi9zNi2iGNmVLRHBzVal2fqphCM8=\ngithub.com/bitnami/go-version v0.0.0-20250505154626-452e8c5ee607/go.mod h1:9iglf1GG4oNRJ39bZ5AZrjgAFD2RwQbXw6Qf7Cs47wo=\ngithub.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=\ngithub.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=\ngithub.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=\ngithub.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=\ngithub.com/bmatcuk/doublestar/v2 v2.0.4 h1:6I6oUiT/sU27eE2OFcWqBhL1SwjyvQuOssxT4a1yidI=\ngithub.com/bmatcuk/doublestar/v2 v2.0.4/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw=\ngithub.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=\ngithub.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=\ngithub.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=\ngithub.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=\ngithub.com/bodgit/sevenzip v1.6.1 h1:kikg2pUMYC9ljU7W9SaqHXhym5HyKm8/M/jd31fYan4=\ngithub.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBEqZh5fQ8=\ngithub.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=\ngithub.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=\ngithub.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M=\ngithub.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0=\ngithub.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70=\ngithub.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=\ngithub.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=\ngithub.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=\ngithub.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=\ngithub.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=\ngithub.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=\ngithub.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=\ngithub.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk=\ngithub.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA=\ngithub.com/charmbracelet/bubbles v0.21.1 h1:nj0decPiixaZeL9diI4uzzQTkkz1kYY8+jgzCZXSmW0=\ngithub.com/charmbracelet/bubbles v0.21.1/go.mod h1:HHvIYRCpbkCJw2yo0vNX1O5loCwSr9/mWS8GYSg50Sk=\ngithub.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=\ngithub.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=\ngithub.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=\ngithub.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=\ngithub.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=\ngithub.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=\ngithub.com/charmbracelet/x/ansi v0.11.5 h1:NBWeBpj/lJPE3Q5l+Lusa4+mH6v7487OP8K0r1IhRg4=\ngithub.com/charmbracelet/x/ansi v0.11.5/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=\ngithub.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=\ngithub.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=\ngithub.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=\ngithub.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=\ngithub.com/chromedp/cdproto v0.0.0-20230802225258-3cf4e6d46a89/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs=\ngithub.com/chromedp/chromedp v0.9.2/go.mod h1:LkSXJKONWTCHAfQasKFUZI+mxqS4tZqhmtGzzhLsnLs=\ngithub.com/chromedp/sysutil v1.0.0/go.mod h1:kgWmDdq8fTzXYcKIBqIYvRRTnYb9aNS9moAV0xufSww=\ngithub.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=\ngithub.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=\ngithub.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=\ngithub.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=\ngithub.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=\ngithub.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=\ngithub.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=\ngithub.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=\ngithub.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=\ngithub.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=\ngithub.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=\ngithub.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=\ngithub.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=\ngithub.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=\ngithub.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=\ngithub.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=\ngithub.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=\ngithub.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=\ngithub.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=\ngithub.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI=\ngithub.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=\ngithub.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=\ngithub.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=\ngithub.com/containerd/cgroups/v3 v3.1.2 h1:OSosXMtkhI6Qove637tg1XgK4q+DhR0mX8Wi8EhrHa4=\ngithub.com/containerd/cgroups/v3 v3.1.2/go.mod h1:PKZ2AcWmSBsY/tJUVhtS/rluX0b1uq1GmPO1ElCmbOw=\ngithub.com/containerd/containerd v1.7.30 h1:/2vezDpLDVGGmkUXmlNPLCCNKHJ5BbC5tJB5JNzQhqE=\ngithub.com/containerd/containerd v1.7.30/go.mod h1:fek494vwJClULlTpExsmOyKCMUAbuVjlFsJQc4/j44M=\ngithub.com/containerd/containerd/api v1.10.0 h1:5n0oHYVBwN4VhoX9fFykCV9dF1/BvAXeg2F8W6UYq1o=\ngithub.com/containerd/containerd/api v1.10.0/go.mod h1:NBm1OAk8ZL+LG8R0ceObGxT5hbUYj7CzTmR3xh0DlMM=\ngithub.com/containerd/containerd/v2 v2.2.1 h1:TpyxcY4AL5A+07dxETevunVS5zxqzuq7ZqJXknM11yk=\ngithub.com/containerd/containerd/v2 v2.2.1/go.mod h1:NR70yW1iDxe84F2iFWbR9xfAN0N2F0NcjTi1OVth4nU=\ngithub.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4=\ngithub.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE=\ngithub.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=\ngithub.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=\ngithub.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=\ngithub.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=\ngithub.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY=\ngithub.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o=\ngithub.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=\ngithub.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=\ngithub.com/containerd/platforms v1.0.0-rc.2 h1:0SPgaNZPVWGEi4grZdV8VRYQn78y+nm6acgLGv/QzE4=\ngithub.com/containerd/platforms v1.0.0-rc.2/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4=\ngithub.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y=\ngithub.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8=\ngithub.com/containerd/stargz-snapshotter/estargz v0.18.1 h1:cy2/lpgBXDA3cDKSyEfNOFMA/c10O1axL69EU7iirO8=\ngithub.com/containerd/stargz-snapshotter/estargz v0.18.1/go.mod h1:ALIEqa7B6oVDsrF37GkGN20SuvG/pIMm7FwP7ZmRb0Q=\ngithub.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ=\ngithub.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o=\ngithub.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40=\ngithub.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk=\ngithub.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=\ngithub.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=\ngithub.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=\ngithub.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/creack/pty v1.1.20 h1:VIPb/a2s17qNeQgDnkfZC35RScx+blkKF8GV68n80J4=\ngithub.com/creack/pty v1.1.20/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=\ngithub.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=\ngithub.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/deitch/magic v0.0.0-20240306090643-c67ab88f10cb h1:4W/2rQ3wzEimF5s+J6OY3ODiQtJZ5W1sForSgogVXkY=\ngithub.com/deitch/magic v0.0.0-20240306090643-c67ab88f10cb/go.mod h1:B3tI9iGHi4imdLi4Asdha1Sc6feLMTfPLXh9IUYmysk=\ngithub.com/derailed/tcell/v2 v2.3.1-rc.4 h1:hrBFOQjcmt1I86Cvcq/NKP7sdRHH+6ibXWBFl0Hn3jY=\ngithub.com/derailed/tcell/v2 v2.3.1-rc.4/go.mod h1:nf68BEL8fjmXQHJT3xZjoZFs2uXOzyJcNAQqGUEMrFY=\ngithub.com/derailed/tview v0.8.5 h1:pogM/OnWlgDo6j4zyzdiIXh7E7+eT7D4CPfBnyaETug=\ngithub.com/derailed/tview v0.8.5/go.mod h1:q+odnnhO6QDPpBT+0dqaWj+X+uoJ6MJehXj9shgP+Cw=\ngithub.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=\ngithub.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=\ngithub.com/diskfs/go-diskfs v1.7.0 h1:vonWmt5CMowXwUc79jWyGrf2DIMeoOjkLlMnQYGVOs8=\ngithub.com/diskfs/go-diskfs v1.7.0/go.mod h1:LhQyXqOugWFRahYUSw47NyZJPezFzB9UELwhpszLP/k=\ngithub.com/distribution/distribution/v3 v3.0.0 h1:q4R8wemdRQDClzoNNStftB2ZAfqOiN6UX90KJc4HjyM=\ngithub.com/distribution/distribution/v3 v3.0.0/go.mod h1:tRNuFoZsUdyRVegq8xGNeds4KLjwLCRin/tTo6i1DhU=\ngithub.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=\ngithub.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=\ngithub.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c=\ngithub.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0=\ngithub.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=\ngithub.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=\ngithub.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM=\ngithub.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=\ngithub.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk=\ngithub.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=\ngithub.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=\ngithub.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=\ngithub.com/docker/docker-credential-helpers v0.9.4 h1:76ItO69/AP/V4yT9V4uuuItG0B1N8hvt0T0c0NN/DzI=\ngithub.com/docker/docker-credential-helpers v0.9.4/go.mod h1:v1S+hepowrQXITkEfw6o4+BMbGot02wiKpzWhGUZK6c=\ngithub.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=\ngithub.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=\ngithub.com/docker/go-events v0.0.0-20250114142523-c867878c5e32 h1:EHZfspsnLAz8Hzccd67D5abwLiqoqym2jz/jOS39mCk=\ngithub.com/docker/go-events v0.0.0-20250114142523-c867878c5e32/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=\ngithub.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8=\ngithub.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw=\ngithub.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=\ngithub.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=\ngithub.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 h1:2tV76y6Q9BB+NEBasnqvs7e49aEBFI8ejC89PSnWH+4=\ngithub.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s=\ngithub.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=\ngithub.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=\ngithub.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=\ngithub.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=\ngithub.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=\ngithub.com/elliotchance/phpserialize v1.4.0 h1:cAp/9+KSnEbUC8oYCE32n2n84BeW8HOY3HMDI8hG2OY=\ngithub.com/elliotchance/phpserialize v1.4.0/go.mod h1:gt7XX9+ETUcLXbtTKEuyrqW3lcLUAeS/AnGZ2e49TZs=\ngithub.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab h1:h1UgjJdAAhj+uPL68n7XASS6bU+07ZX1WJvVS2eyoeY=\ngithub.com/elliotwutingfeng/asciiset v0.0.0-20230602022725-51bbb787efab/go.mod h1:GLo/8fDswSAniFG+BFIaiSPcK610jyzgEhWYPQwuQdw=\ngithub.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=\ngithub.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=\ngithub.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=\ngithub.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=\ngithub.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=\ngithub.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=\ngithub.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=\ngithub.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=\ngithub.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=\ngithub.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ=\ngithub.com/envoyproxy/go-control-plane v0.13.4 h1:zEqyPVyku6IvWCFwux4x9RxkLOMUL+1vC9xUFv5l2/M=\ngithub.com/envoyproxy/go-control-plane v0.13.4/go.mod h1:kDfuBlDVsSj2MjrLEtRWtHlsWIFcGyB2RMO44Dc5GZA=\ngithub.com/envoyproxy/go-control-plane/envoy v1.32.4 h1:jb83lalDRZSpPWW2Z7Mck/8kXZ5CQAFYVjQcdVIr83A=\ngithub.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1Zm+wSYE20UrLtt7JZMWiWQXQEw=\ngithub.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=\ngithub.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=\ngithub.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=\ngithub.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws=\ngithub.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=\ngithub.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=\ngithub.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=\ngithub.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8=\ngithub.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=\ngithub.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4=\ngithub.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc=\ngithub.com/facebookincubator/flog v0.0.0-20190930132826-d2511d0ce33c/go.mod h1:QGzNH9ujQ2ZUr/CjDGZGWeDAVStrWNjHeEcjJL96Nuk=\ngithub.com/facebookincubator/nvdtools v0.1.5 h1:jbmDT1nd6+k+rlvKhnkgMokrCAzHoASWE5LtHbX2qFQ=\ngithub.com/facebookincubator/nvdtools v0.1.5/go.mod h1:Kh55SAWnjckS96TBSrXI99KrEKH4iB0OJby3N8GRJO4=\ngithub.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8=\ngithub.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc=\ngithub.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=\ngithub.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=\ngithub.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=\ngithub.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=\ngithub.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=\ngithub.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA=\ngithub.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI=\ngithub.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=\ngithub.com/felixge/fgprof v0.9.5 h1:8+vR6yu2vvSKn08urWyEuxx75NWPEvybbkBirEpsbVY=\ngithub.com/felixge/fgprof v0.9.5/go.mod h1:yKl+ERSa++RYOs32d8K6WEXCB4uXdLls4ZaZPpayhMM=\ngithub.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=\ngithub.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=\ngithub.com/foxcpp/go-mockdns v1.2.0 h1:omK3OrHRD1IWJz1FuFBCFquhXslXoF17OvBS6JPzZF0=\ngithub.com/foxcpp/go-mockdns v1.2.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk=\ngithub.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=\ngithub.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=\ngithub.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw=\ngithub.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0=\ngithub.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=\ngithub.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=\ngithub.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=\ngithub.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=\ngithub.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=\ngithub.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=\ngithub.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=\ngithub.com/github/go-spdx/v2 v2.3.6 h1:9flm625VmmTlWXi0YH5W9V8FdMfulvxalHdYnUfoqxc=\ngithub.com/github/go-spdx/v2 v2.3.6/go.mod h1:/5rwgS0txhGtRdUZwc02bTglzg6HK3FfuEbECKlK2Sg=\ngithub.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=\ngithub.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=\ngithub.com/gkampitakis/go-snaps v0.5.19 h1:hUJlCQOpTt1M+kSisMwioDWZDWpDtdAvUhvWCx1YGW0=\ngithub.com/gkampitakis/go-snaps v0.5.19/go.mod h1:gC3YqxQTPyIXvQrw/Vpt3a8VqR1MO8sVpZFWN4DGwNs=\ngithub.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=\ngithub.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=\ngithub.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=\ngithub.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=\ngithub.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=\ngithub.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=\ngithub.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk=\ngithub.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=\ngithub.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=\ngithub.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=\ngithub.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=\ngithub.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=\ngithub.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=\ngithub.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=\ngithub.com/go-git/go-git/v5 v5.16.5 h1:mdkuqblwr57kVfXri5TTH+nMFLNUxIj9Z7F5ykFbw5s=\ngithub.com/go-git/go-git/v5 v5.16.5/go.mod h1:QOMLpNf1qxuSY4StA/ArOdfFR2TrKEjJiye2kel2m+M=\ngithub.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=\ngithub.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs=\ngithub.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw=\ngithub.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI=\ngithub.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo=\ngithub.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=\ngithub.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=\ngithub.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=\ngithub.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=\ngithub.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=\ngithub.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=\ngithub.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=\ngithub.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=\ngithub.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=\ngithub.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=\ngithub.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=\ngithub.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=\ngithub.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=\ngithub.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc=\ngithub.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk=\ngithub.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=\ngithub.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=\ngithub.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=\ngithub.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=\ngithub.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=\ngithub.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=\ngithub.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=\ngithub.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=\ngithub.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=\ngithub.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=\ngithub.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro=\ngithub.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=\ngithub.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=\ngithub.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=\ngithub.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=\ngithub.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=\ngithub.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=\ngithub.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=\ngithub.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=\ngithub.com/gocsaf/csaf/v3 v3.5.1 h1:jTA1fLrK0/JIczPs7itTD53qANoO4tn2VaGvUeitePc=\ngithub.com/gocsaf/csaf/v3 v3.5.1/go.mod h1:pga89lE+iWJm7smTdzYcXuetYUbgY8caXfaIP4BJG98=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=\ngithub.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=\ngithub.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=\ngithub.com/gohugoio/hashstructure v0.6.0 h1:7wMB/2CfXoThFYhdWRGv3u3rUM761Cq29CxUW+NltUg=\ngithub.com/gohugoio/hashstructure v0.6.0/go.mod h1:lapVLk9XidheHG1IQ4ZSbyYrXcaILU1ZEP/+vno5rBQ=\ngithub.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=\ngithub.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=\ngithub.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=\ngithub.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=\ngithub.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=\ngithub.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=\ngithub.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=\ngithub.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=\ngithub.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=\ngithub.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=\ngithub.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=\ngithub.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=\ngithub.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=\ngithub.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=\ngithub.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=\ngithub.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=\ngithub.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=\ngithub.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=\ngithub.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=\ngithub.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/go-containerregistry v0.20.7 h1:24VGNpS0IwrOZ2ms2P1QE3Xa5X9p4phx0aUgzYzHW6I=\ngithub.com/google/go-containerregistry v0.20.7/go.mod h1:Lx5LCZQjLH1QBaMPeGwsME9biPeo1lPx6lbGj/UmzgM=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs=\ngithub.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY=\ngithub.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no=\ngithub.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=\ngithub.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=\ngithub.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=\ngithub.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc=\ngithub.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0=\ngithub.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=\ngithub.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=\ngithub.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=\ngithub.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=\ngithub.com/google/pprof v0.0.0-20240227163752-401108e1b7e7/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=\ngithub.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5 h1:xhMrHhTJ6zxu3gA4enFM9MLn9AY7613teCdFnlUVbSQ=\ngithub.com/google/pprof v0.0.0-20250630185457-6e76a2b096b5/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA=\ngithub.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=\ngithub.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=\ngithub.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ=\ngithub.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA=\ngithub.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=\ngithub.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=\ngithub.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=\ngithub.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=\ngithub.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo=\ngithub.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc=\ngithub.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=\ngithub.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=\ngithub.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg=\ngithub.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=\ngithub.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=\ngithub.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=\ngithub.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=\ngithub.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=\ngithub.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=\ngithub.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=\ngithub.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=\ngithub.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY=\ngithub.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo=\ngithub.com/gpustack/gguf-parser-go v0.23.1 h1:0U7DOrsi7ryx2L/dlMy+BSQ5bJV4AuMEIgGBs4RK46A=\ngithub.com/gpustack/gguf-parser-go v0.23.1/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0=\ngithub.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA=\ngithub.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=\ngithub.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=\ngithub.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=\ngithub.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4=\ngithub.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=\ngithub.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70 h1:0HADrxxqaQkGycO1JoUUA+B4FnIkuo8d2bz/hSaTFFQ=\ngithub.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.70/go.mod h1:fm2FdDCzJdtbXF7WKAMvBb5NEPouXPHFbGNYs9ShFns=\ngithub.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M=\ngithub.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms=\ngithub.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=\ngithub.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=\ngithub.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=\ngithub.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=\ngithub.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=\ngithub.com/hashicorp/go-getter v1.8.4 h1:hGEd2xsuVKgwkMtPVufq73fAmZU/x65PPcqH3cb0D9A=\ngithub.com/hashicorp/go-getter v1.8.4/go.mod h1:x27pPGSg9kzoB147QXI8d/nDvp2IgYGcwuRjpaXE9Yg=\ngithub.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=\ngithub.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=\ngithub.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=\ngithub.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=\ngithub.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=\ngithub.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=\ngithub.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=\ngithub.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=\ngithub.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=\ngithub.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=\ngithub.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=\ngithub.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=\ngithub.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4=\ngithub.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=\ngithub.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=\ngithub.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=\ngithub.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=\ngithub.com/hashicorp/golang-lru/arc/v2 v2.0.5 h1:l2zaLDubNhW4XO3LnliVj0GXO3+/CGNJAg1dcN2Fpfw=\ngithub.com/hashicorp/golang-lru/arc/v2 v2.0.5/go.mod h1:ny6zBSQZi2JxIeYcv7kt2sH2PXJtirBN7RDhRpxPkxU=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=\ngithub.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=\ngithub.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=\ngithub.com/hashicorp/hcl/v2 v2.24.0 h1:2QJdZ454DSsYGoaE6QheQZjtKZSUs9Nh2izTWiwQxvE=\ngithub.com/hashicorp/hcl/v2 v2.24.0/go.mod h1:oGoO1FIQYfn/AgyOhlg9qLC6/nOJPX3qGbkZpYAcqfM=\ngithub.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=\ngithub.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY=\ngithub.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc=\ngithub.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=\ngithub.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE=\ngithub.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk=\ngithub.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4=\ngithub.com/henvic/httpretty v0.1.4 h1:Jo7uwIRWVFxkqOnErcoYfH90o3ddQyVrSANeS4cxYmU=\ngithub.com/henvic/httpretty v0.1.4/go.mod h1:Dn60sQTZfbt2dYsdUSNsCljyF4AfdqnuJFDLJA1I4AM=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI=\ngithub.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=\ngithub.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=\ngithub.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=\ngithub.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=\ngithub.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=\ngithub.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=\ngithub.com/ianlancetaylor/demangle v0.0.0-20230524184225-eabc099b10ab/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw=\ngithub.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=\ngithub.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=\ngithub.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=\ngithub.com/itchyny/gojq v0.12.18 h1:gFGHyt/MLbG9n6dqnvlliiya2TaMMh6FFaR2b1H6Drc=\ngithub.com/itchyny/gojq v0.12.18/go.mod h1:4hPoZ/3lN9fDL1D+aK7DY1f39XZpY9+1Xpjz8atrEkg=\ngithub.com/itchyny/timefmt-go v0.1.7 h1:xyftit9Tbw+Dc/huSSPJaEmX1TVL8lw5vxjJLK4GMMA=\ngithub.com/itchyny/timefmt-go v0.1.7/go.mod h1:5E46Q+zj7vbTgWY8o5YkMeYb4I6GeWLFnetPy5oBrAI=\ngithub.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=\ngithub.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=\ngithub.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=\ngithub.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=\ngithub.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=\ngithub.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=\ngithub.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=\ngithub.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=\ngithub.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=\ngithub.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=\ngithub.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=\ngithub.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=\ngithub.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=\ngithub.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=\ngithub.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=\ngithub.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=\ngithub.com/karrick/godirwalk v1.17.0 h1:b4kY7nqDdioR/6qnbHQyDvmA17u5G1cZ6J+CZXwSWoI=\ngithub.com/karrick/godirwalk v1.17.0/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk=\ngithub.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 h1:WdAeg/imY2JFPc/9CST4bZ80nNJbiBFCAdSZCSgrS5Y=\ngithub.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953/go.mod h1:6o+UrvuZWc4UTyBhQf0LGjW9Ld7qJxLz/OqvSOWWlEc=\ngithub.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=\ngithub.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=\ngithub.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=\ngithub.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=\ngithub.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=\ngithub.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=\ngithub.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=\ngithub.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=\ngithub.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=\ngithub.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=\ngithub.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f h1:GvCU5GXhHq+7LeOzx/haG7HSIZokl3/0GkoUFzsRJjg=\ngithub.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f/go.mod h1:q59u9px8b7UTj0nIjEjvmTWekazka6xIt6Uogz5Dm+8=\ngithub.com/knqyf263/go-deb-version v0.0.0-20241115132648-6f4aee6ccd23 h1:dWzdsqjh1p2gNtRKqNwuBvKqMNwnLOPLzVZT1n6DK7s=\ngithub.com/knqyf263/go-deb-version v0.0.0-20241115132648-6f4aee6ccd23/go.mod h1:lUaIXCWzf7BRKTY5iEcrYy1TfgbYLYVIS/B2vPkJzOc=\ngithub.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=\ngithub.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=\ngithub.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=\ngithub.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=\ngithub.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=\ngithub.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=\ngithub.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=\ngithub.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=\ngithub.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=\ngithub.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=\ngithub.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=\ngithub.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=\ngithub.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE=\ngithub.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY=\ngithub.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc=\ngithub.com/lmittmann/tint v1.1.3 h1:Hv4EaHWXQr+GTFnOU4VKf8UvAtZgn0VuKT+G0wFlO3I=\ngithub.com/lmittmann/tint v1.1.3/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=\ngithub.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=\ngithub.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=\ngithub.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=\ngithub.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=\ngithub.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w=\ngithub.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=\ngithub.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=\ngithub.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=\ngithub.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=\ngithub.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4=\ngithub.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU=\ngithub.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=\ngithub.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=\ngithub.com/masahiro331/go-mvn-version v0.0.0-20250131095131-f4974fa13b8a h1:eLvAzVoRfHEOl64OxFhepPf3vj7SKvXY/tFc3BS0b7s=\ngithub.com/masahiro331/go-mvn-version v0.0.0-20250131095131-f4974fa13b8a/go.mod h1:jZ3F25l7DbD7l7DcA8aj7eo1EZ84nbzcQHBB4lCSrI8=\ngithub.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=\ngithub.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=\ngithub.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=\ngithub.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=\ngithub.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=\ngithub.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84=\ngithub.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=\ngithub.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=\ngithub.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw=\ngithub.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=\ngithub.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=\ngithub.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=\ngithub.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=\ngithub.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=\ngithub.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=\ngithub.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=\ngithub.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/mholt/archives v0.1.5 h1:Fh2hl1j7VEhc6DZs2DLMgiBNChUux154a1G+2esNvzQ=\ngithub.com/mholt/archives v0.1.5/go.mod h1:3TPMmBLPsgszL+1As5zECTuKwKvIfj6YcwWPpeTAXF4=\ngithub.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=\ngithub.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso=\ngithub.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI=\ngithub.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=\ngithub.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=\ngithub.com/mikelolasagasti/xz v1.0.1 h1:Q2F2jX0RYJUG3+WsM+FJknv+6eVjsjXNDV0KJXZzkD0=\ngithub.com/mikelolasagasti/xz v1.0.1/go.mod h1:muAirjiOUxPRXwm9HdDtB3uoRPrGnL85XHtokL9Hcgc=\ngithub.com/minio/minlz v1.0.1 h1:OUZUzXcib8diiX+JYxyRLIdomyZYzHct6EShOKtQY2A=\ngithub.com/minio/minlz v1.0.1/go.mod h1:qT0aEB35q79LLornSzeDH75LBf3aH1MV+jB5w9Wasec=\ngithub.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=\ngithub.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=\ngithub.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=\ngithub.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=\ngithub.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=\ngithub.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=\ngithub.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=\ngithub.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=\ngithub.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=\ngithub.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=\ngithub.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=\ngithub.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=\ngithub.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=\ngithub.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=\ngithub.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg=\ngithub.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc=\ngithub.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=\ngithub.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=\ngithub.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=\ngithub.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=\ngithub.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg=\ngithub.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4=\ngithub.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=\ngithub.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=\ngithub.com/moby/sys/signal v0.7.1 h1:PrQxdvxcGijdo6UXXo/lU/TvHUWyPhj7UOpSo8tuvk0=\ngithub.com/moby/sys/signal v0.7.1/go.mod h1:Se1VGehYokAkrSQwL4tDzHvETwUZlnY7S5XtQ50mQp8=\ngithub.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=\ngithub.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=\ngithub.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=\ngithub.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=\ngithub.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=\ngithub.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=\ngithub.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0=\ngithub.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4=\ngithub.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=\ngithub.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=\ngithub.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=\ngithub.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=\ngithub.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=\ngithub.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=\ngithub.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=\ngithub.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=\ngithub.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=\ngithub.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=\ngithub.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=\ngithub.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=\ngithub.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=\ngithub.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1 h1:kpt9ZfKcm+EDG4s40hMwE//d5SBgDjUOrITReV2u4aA=\ngithub.com/nix-community/go-nix v0.0.0-20250101154619-4bdde671e0a1/go.mod h1:qgCw4bBKZX8qMgGeEZzGFVT3notl42dBjNqO2jut0M0=\ngithub.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 h1:NHrXEjTNQY7P0Zfx1aMrNhpgxHmow66XQtm0aQLY0AE=\ngithub.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249/go.mod h1:mpRZBD8SJ55OIICQ3iWH0Yz3cjzA61JdqMLoWXeB2+8=\ngithub.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A=\ngithub.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=\ngithub.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=\ngithub.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=\ngithub.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=\ngithub.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj4EljqMiZsIcE09mmF8XsD5AYOJc=\ngithub.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0=\ngithub.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=\ngithub.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=\ngithub.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0 h1:jrYnow5+hy3WRDCBypUFvVKNSPPCdqgSXIE9eJDD8LM=\ngithub.com/olekukonko/ll v0.1.4-0.20260115111900-9e59c2286df0/go.mod h1:b52bVQRRPObe+yyBl0TxNfhesL0nedD4Cht0/zx55Ew=\ngithub.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA=\ngithub.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=\ngithub.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=\ngithub.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=\ngithub.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=\ngithub.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=\ngithub.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=\ngithub.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=\ngithub.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=\ngithub.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=\ngithub.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=\ngithub.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=\ngithub.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=\ngithub.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=\ngithub.com/opencontainers/runtime-spec v1.3.0 h1:YZupQUdctfhpZy3TM39nN9Ika5CBWT5diQ8ibYCRkxg=\ngithub.com/opencontainers/runtime-spec v1.3.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=\ngithub.com/opencontainers/selinux v1.13.1 h1:A8nNeceYngH9Ow++M+VVEwJVpdFmrlxsN22F+ISDCJE=\ngithub.com/opencontainers/selinux v1.13.1/go.mod h1:S10WXZ/osk2kWOYKy1x2f/eXF5ZHJoUs8UU/2caNRbg=\ngithub.com/openvex/go-vex v0.2.7 h1:/pN3bqvS4QOc6WkkL0hbKzJuAtsUD9vmvk9IZkzD3Zc=\ngithub.com/openvex/go-vex v0.2.7/go.mod h1:ZyQC3NXl9jjS53JOpBG3LAUXySkW8IlJ/GIhsnf5D54=\ngithub.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=\ngithub.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554 h1:FvA4bwjKpPqik5WsQ8+4z4DKWgA1tO1RTTtNKr5oYNA=\ngithub.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554/go.mod h1:n73K/hcuJ50MiVznXyN4rde6fZY7naGKWBXOLFTyc94=\ngithub.com/package-url/packageurl-go v0.1.3 h1:4juMED3hHiz0set3Vq3KeQ75KD1avthoXLtmE3I0PLs=\ngithub.com/package-url/packageurl-go v0.1.3/go.mod h1:nKAWB8E6uk1MHqiS/lQb9pYBGH2+mdJ2PJc2s50dQY0=\ngithub.com/pandatix/go-cvss v0.6.2 h1:TFiHlzUkT67s6UkelHmK6s1INKVUG7nlKYiWWDTITGI=\ngithub.com/pandatix/go-cvss v0.6.2/go.mod h1:jDXYlQBZrc8nvrMUVVvTG8PhmuShOnKrxP53nOFkt8Q=\ngithub.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=\ngithub.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM=\ngithub.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrpz5K6Vw=\ngithub.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=\ngithub.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=\ngithub.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI=\ngithub.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=\ngithub.com/petergtz/pegomock v2.9.0+incompatible h1:BKfb5XfkJfehe5T+O1xD4Zm26Sb9dnRj7tHxLYwUPiI=\ngithub.com/petergtz/pegomock v2.9.0+incompatible/go.mod h1:nuBLWZpVyv/fLo56qTwt/AUau7jgouO1h7bEvZCq82o=\ngithub.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI=\ngithub.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE=\ngithub.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=\ngithub.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=\ngithub.com/pjbgf/sha1cd v0.4.0 h1:NXzbL1RvjTUi6kgYZCX3fPwwl27Q1LJndxtUDVfJGRY=\ngithub.com/pjbgf/sha1cd v0.4.0/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=\ngithub.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=\ngithub.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=\ngithub.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=\ngithub.com/pkg/xattr v0.4.12 h1:rRTkSyFNTRElv6pkA3zpjHpQ90p/OdHQC1GmGh1aTjM=\ngithub.com/pkg/xattr v0.4.12/go.mod h1:di8WF84zAKk8jzR1UBTEWh9AUlIZZ7M/JNt8e9B6ktU=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo=\ngithub.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=\ngithub.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=\ngithub.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY=\ngithub.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg=\ngithub.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=\ngithub.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=\ngithub.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=\ngithub.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=\ngithub.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=\ngithub.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=\ngithub.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=\ngithub.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=\ngithub.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=\ngithub.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=\ngithub.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=\ngithub.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=\ngithub.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=\ngithub.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=\ngithub.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=\ngithub.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=\ngithub.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=\ngithub.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=\ngithub.com/rakyll/hey v0.1.5 h1:oc3QhpT8ETXcr5xIE2xgWYNSNA/Z52XA20ku9hWCchY=\ngithub.com/rakyll/hey v0.1.5/go.mod h1:tLUK++7gal6z92HvVEaP4QfIhEb5cYywEJgrUqmqt7Y=\ngithub.com/redis/go-redis/extra/rediscmd/v9 v9.0.5 h1:EaDatTxkdHG+U3Bk4EUr+DZ7fOGwTfezUiUJMaIcaho=\ngithub.com/redis/go-redis/extra/rediscmd/v9 v9.0.5/go.mod h1:fyalQWdtzDBECAQFBJuQe5bzQ02jGd5Qcbgb97Flm7U=\ngithub.com/redis/go-redis/extra/redisotel/v9 v9.0.5 h1:EfpWLLCyXw8PSM2/XNJLjI3Pb27yVE+gIAfeqp8LUCc=\ngithub.com/redis/go-redis/extra/redisotel/v9 v9.0.5/go.mod h1:WZjPDy7VNzn77AAfnAfVjZNvfJTYfPetfZk5yoSTLaQ=\ngithub.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=\ngithub.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=\ngithub.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=\ngithub.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=\ngithub.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=\ngithub.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=\ngithub.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=\ngithub.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/rubenv/sql-migrate v1.8.1 h1:EPNwCvjAowHI3TnZ+4fQu3a915OpnQoPAjTXCGOy2U0=\ngithub.com/rubenv/sql-migrate v1.8.1/go.mod h1:BTIKBORjzyxZDS6dzoiw6eAFYJ1iNlGAtjn4LGeVjS8=\ngithub.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=\ngithub.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=\ngithub.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c h1:8gOLsYwaY2JwlTMT4brS5/9XJdrdIbmk2obvQ748CC0=\ngithub.com/rust-secure-code/go-rustaudit v0.0.0-20250226111315-e20ec32e963c/go.mod h1:kwM/7r/rVluTE8qJbHAffduuqmSv4knVQT2IajGvSiA=\ngithub.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=\ngithub.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=\ngithub.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig=\ngithub.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=\ngithub.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=\ngithub.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA=\ngithub.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y=\ngithub.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA=\ngithub.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU=\ngithub.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg=\ngithub.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U=\ngithub.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ=\ngithub.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=\ngithub.com/sassoftware/go-rpmutils v0.4.0 h1:ojND82NYBxgwrV+mX1CWsd5QJvvEZTKddtCdFLPWhpg=\ngithub.com/sassoftware/go-rpmutils v0.4.0/go.mod h1:3goNWi7PGAT3/dlql2lv3+MSN5jNYPjT5mVcQcIsYzI=\ngithub.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e h1:7q6NSFZDeGfvvtIRwBrU/aegEYJYmvev0cHAwo17zZQ=\ngithub.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs=\ngithub.com/scylladb/termtables v0.0.0-20191203121021-c4c0b6d42ff4/go.mod h1:C1a7PQSMz9NShzorzCiG2fk9+xuCgLkPeCvMHYR2OWg=\ngithub.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=\ngithub.com/sebdah/goldie/v2 v2.7.1 h1:PkBHymaYdtvEkZV7TmyqKxdmn5/Vcj+8TpATWZjnG5E=\ngithub.com/sebdah/goldie/v2 v2.7.1/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI=\ngithub.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=\ngithub.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=\ngithub.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=\ngithub.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=\ngithub.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=\ngithub.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=\ngithub.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=\ngithub.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=\ngithub.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=\ngithub.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af h1:Sp5TG9f7K39yfB+If0vjp97vuT74F72r8hfRpP8jLU0=\ngithub.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=\ngithub.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=\ngithub.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=\ngithub.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d h1:3VwvTjiRPA7cqtgOWddEL+JrcijMlXUmj99c/6YyZoY=\ngithub.com/smallnest/ringbuffer v0.0.0-20241116012123-461381446e3d/go.mod h1:tAG61zBM1DYRaGIPloumExGvScf08oHuo0kFoOqdbT0=\ngithub.com/sorairolake/lzip-go v0.3.8 h1:j5Q2313INdTA80ureWYRhX+1K78mUXfMoPZCw/ivWik=\ngithub.com/sorairolake/lzip-go v0.3.8/go.mod h1:JcBqGMV0frlxwrsE9sMWXDjqn3EeVf0/54YPsw66qkU=\ngithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=\ngithub.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=\ngithub.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=\ngithub.com/spdx/gordf v0.0.0-20250128162952-000978ccd6fb h1:7G2Czq97VORM5xNRrD8tSQdhoXPRs8s+Otlc7st9TS0=\ngithub.com/spdx/gordf v0.0.0-20250128162952-000978ccd6fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM=\ngithub.com/spdx/tools-golang v0.5.7 h1:+sWcKGnhwp3vLdMqPcLdA6QK679vd86cK9hQWH3AwCg=\ngithub.com/spdx/tools-golang v0.5.7/go.mod h1:jg7w0LOpoNAw6OxKEzCoqPC2GCTj45LyTlVmXubDsYw=\ngithub.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=\ngithub.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=\ngithub.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=\ngithub.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=\ngithub.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=\ngithub.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=\ngithub.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=\ngithub.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4=\ngithub.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=\ngithub.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=\ngithub.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=\ngithub.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM=\ngithub.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=\ngithub.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=\ngithub.com/spiffe/go-spiffe/v2 v2.5.0 h1:N2I01KCUkv1FAjZXJMwh95KK1ZIQLYbPfhaxw8WS0hE=\ngithub.com/spiffe/go-spiffe/v2 v2.5.0/go.mod h1:P+NxobPc6wXhVtINNtFjNWGBTreew1GBUCwT2wPmb7g=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=\ngithub.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=\ngithub.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=\ngithub.com/sylabs/sif/v2 v2.22.0 h1:Y+xXufp4RdgZe02SR3nWEg7S6q4tPWN237WHYzkDSKA=\ngithub.com/sylabs/sif/v2 v2.22.0/go.mod h1:W1XhWTmG1KcG7j5a3KSYdMcUIFvbs240w/MMVW627hs=\ngithub.com/sylabs/squashfs v1.0.6 h1:PvJcDzxr+vIm2kH56mEMbaOzvGu79gK7P7IX+R7BDZI=\ngithub.com/sylabs/squashfs v1.0.6/go.mod h1:DlDeUawVXLWAsSRa085Eo0ZenGzAB32JdAUFaB0LZfE=\ngithub.com/terminalstatic/go-xsd-validate v0.1.6 h1:TenYeQ3eY631qNi1/cTmLH/s2slHPRKTTHT+XSHkepo=\ngithub.com/terminalstatic/go-xsd-validate v0.1.6/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw=\ngithub.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=\ngithub.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=\ngithub.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=\ngithub.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=\ngithub.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=\ngithub.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=\ngithub.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=\ngithub.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=\ngithub.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=\ngithub.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=\ngithub.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=\ngithub.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=\ngithub.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=\ngithub.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=\ngithub.com/vbatts/go-mtree v0.7.0 h1:ytmOc3MTRidZiBi9VBCyZ2BHe4fZS47L5v7BVXDWW4E=\ngithub.com/vbatts/go-mtree v0.7.0/go.mod h1:EjdpFC+LZy1TXbRGNa1MKKgjQ+7ew3foMFJK8o4/TdY=\ngithub.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4=\ngithub.com/vbatts/tar-split v0.12.2/go.mod h1:eF6B6i6ftWQcDqEn3/iGFRFRo8cBIMSJVOpnNdfTMFA=\ngithub.com/vifraa/gopom v1.0.0 h1:L9XlKbyvid8PAIK8nr0lihMApJQg/12OBvMA28BcWh0=\ngithub.com/vifraa/gopom v1.0.0/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o=\ngithub.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc=\ngithub.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=\ngithub.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 h1:jIVmlAFIqV3d+DOxazTR9v+zgj8+VYuQBzPgBZvWBHA=\ngithub.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651/go.mod h1:b26F2tHLqaoRQf8DywqzVaV1MQ9yvjb0OMcNl7Nxu20=\ngithub.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b h1:uWNQ0khA6RdFzODOMwKo9XXu7fuewnnkHykUtuKru8s=\ngithub.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b/go.mod h1:ewlIKbKV8l+jCj8rkdXIs361ocR5x3qGyoCSca47Gx8=\ngithub.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 h1:0KGbf+0SMg+UFy4e1A/CPVvXn21f1qtWdeJwxZFoQG8=\ngithub.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA=\ngithub.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=\ngithub.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=\ngithub.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=\ngithub.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=\ngithub.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=\ngithub.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=\ngithub.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=\ngithub.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=\ngithub.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo=\ngithub.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos=\ngithub.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ=\ngithub.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngithub.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=\ngithub.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=\ngithub.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngithub.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngithub.com/zclconf/go-cty v1.14.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=\ngithub.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk=\ngithub.com/zclconf/go-cty v1.16.3/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=\ngithub.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940 h1:4r45xpDWB6ZMSMNJFMOjqrGHynW3DIBuR2H9j0ug+Mo=\ngithub.com/zclconf/go-cty-debug v0.0.0-20240509010212-0d6042c53940/go.mod h1:CmBdvvj3nqzfzJ6nTCIwDTPZ56aVGvDrmztiO5g3qrM=\ngithub.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=\ngithub.com/zeebo/errs v1.4.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4=\ngo.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=\ngo.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=\ngo.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=\ngo.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=\ngo.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs=\ngo.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=\ngo.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=\ngo.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=\ngo.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=\ngo.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=\ngo.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=\ngo.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=\ngo.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=\ngo.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=\ngo.opentelemetry.io/contrib/bridges/prometheus v0.57.0 h1:UW0+QyeyBVhn+COBec3nGhfnFe5lwB0ic1JBVjzhk0w=\ngo.opentelemetry.io/contrib/bridges/prometheus v0.57.0/go.mod h1:ppciCHRLsyCio54qbzQv0E4Jyth/fLWDTJYfvWpcSVk=\ngo.opentelemetry.io/contrib/detectors/gcp v1.37.0 h1:B+WbN9RPsvobe6q4vP6KgM8/9plR/HNjgGBrfcOlweA=\ngo.opentelemetry.io/contrib/detectors/gcp v1.37.0/go.mod h1:K5zQ3TT7p2ru9Qkzk0bKtCql0RGkPj9pRjpXgZJZ+rU=\ngo.opentelemetry.io/contrib/exporters/autoexport v0.57.0 h1:jmTVJ86dP60C01K3slFQa2NQ/Aoi7zA+wy7vMOKD9H4=\ngo.opentelemetry.io/contrib/exporters/autoexport v0.57.0/go.mod h1:EJBheUMttD/lABFyLXhce47Wr6DPWYReCzaZiXadH7g=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo=\ngo.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0 h1:Hf9xI/XLML9ElpiHVDNwvqI0hIFlzV8dgIr35kV1kRU=\ngo.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.62.0/go.mod h1:NfchwuyNoMcZ5MLHwPrODwUF1HWCXWrL31s8gSAdIKY=\ngo.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=\ngo.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=\ngo.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0 h1:WzNab7hOOLzdDF/EoWCt4glhrbMPVMOO5JYTmpz36Ls=\ngo.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.8.0/go.mod h1:hKvJwTzJdp90Vh7p6q/9PAOd55dI6WA6sWj62a/JvSs=\ngo.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0 h1:S+LdBGiQXtJdowoJoQPEtI52syEP/JYBUpjO49EQhV8=\ngo.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.8.0/go.mod h1:5KXybFvPGds3QinJWQT7pmXf+TN5YIa7CNYObWRkj50=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0 h1:j7ZSD+5yn+lo3sGV69nW04rRR0jhYnBwjuX3r0HvnK0=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.32.0/go.mod h1:WXbYJTUaZXAbYd8lbgGuvih0yuCfOFC5RJoYnoLcGz8=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0 h1:t/Qur3vKSkUCcDVaSumWF2PKHt85pc7fRvFuoVT8qFU=\ngo.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.32.0/go.mod h1:Rl61tySSdcOJWoEgYZVtmnKdA0GeKrSqkHC1t+91CH8=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0 h1:1fTNlAIJZGWLP5FVu0fikVry1IsiUnXjf7QFvoNN3Xw=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace v1.35.0/go.mod h1:zjPK58DtkqQFn+YUMbx0M2XV3QgKU0gS9LeGohREyK4=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0 h1:m639+BofXTvcY1q8CGs4ItwQarYtJPOWmVobfM1HpVI=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.35.0/go.mod h1:LjReUci/F4BUyv+y4dwnq3h/26iNOeC3wAIqgvTIZVo=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=\ngo.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=\ngo.opentelemetry.io/otel/exporters/prometheus v0.54.0 h1:rFwzp68QMgtzu9PgP3jm9XaMICI6TsofWWPcBDKwlsU=\ngo.opentelemetry.io/otel/exporters/prometheus v0.54.0/go.mod h1:QyjcV9qDP6VeK5qPyKETvNjmaaEc7+gqjh4SS0ZYzDU=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0 h1:CHXNXwfKWfzS65yrlB2PVds1IBZcdsX8Vepy9of0iRU=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutlog v0.8.0/go.mod h1:zKU4zUgKiaRxrdovSS2amdM5gOc59slmo/zJwGX+YBg=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI=\ngo.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w=\ngo.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0 h1:cC2yDI3IQd0Udsux7Qmq8ToKAx1XCilTQECZ0KDZyTw=\ngo.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.32.0/go.mod h1:2PD5Ex6z8CFzDbTdOlwyNIUywRr1DN0ospafJM1wJ+s=\ngo.opentelemetry.io/otel/log v0.8.0 h1:egZ8vV5atrUWUbnSsHn6vB8R21G2wrKqNiDt3iWertk=\ngo.opentelemetry.io/otel/log v0.8.0/go.mod h1:M9qvDdUTRCopJcGRKg57+JSQ9LgLBrwwfC32epk5NX8=\ngo.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=\ngo.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=\ngo.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=\ngo.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=\ngo.opentelemetry.io/otel/sdk/log v0.8.0 h1:zg7GUYXqxk1jnGF/dTdLPrK06xJdrXgqgFLnI4Crxvs=\ngo.opentelemetry.io/otel/sdk/log v0.8.0/go.mod h1:50iXr0UVwQrYS45KbruFrEt4LvAdCaWWgIrsN3ZQggo=\ngo.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=\ngo.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=\ngo.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=\ngo.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=\ngo.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=\ngo.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=\ngo.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=\ngo.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=\ngo.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=\ngo.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=\ngo.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=\ngo.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=\ngo.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=\ngo.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngo4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=\ngo4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=\ngolang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=\ngolang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=\ngolang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=\ngolang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=\ngolang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=\ngolang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=\ngolang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=\ngolang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=\ngolang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=\ngolang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=\ngolang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=\ngolang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=\ngolang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=\ngolang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=\ngolang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=\ngolang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=\ngolang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=\ngolang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=\ngolang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=\ngolang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=\ngolang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=\ngolang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=\ngolang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=\ngolang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=\ngolang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=\ngolang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=\ngolang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=\ngolang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=\ngolang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=\ngolang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=\ngolang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=\ngolang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=\ngolang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=\ngolang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=\ngolang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=\ngolang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=\ngolang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=\ngolang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=\ngolang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=\ngolang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=\ngolang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=\ngolang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=\ngolang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=\ngolang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=\ngolang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=\ngolang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=\ngolang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=\ngolang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=\ngolang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=\ngolang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=\ngolang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=\ngolang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=\ngolang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da h1:noIWHXmPHxILtqtCOPIhSt0ABwskkZKjD3bXGnZGpNY=\ngolang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=\ngonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=\ngonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=\ngoogle.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=\ngoogle.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=\ngoogle.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=\ngoogle.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=\ngoogle.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=\ngoogle.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=\ngoogle.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=\ngoogle.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=\ngoogle.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=\ngoogle.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=\ngoogle.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=\ngoogle.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=\ngoogle.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=\ngoogle.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo=\ngoogle.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4=\ngoogle.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw=\ngoogle.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU=\ngoogle.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k=\ngoogle.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=\ngoogle.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE=\ngoogle.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI=\ngoogle.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=\ngoogle.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=\ngoogle.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw=\ngoogle.golang.org/api v0.256.0 h1:u6Khm8+F9sxbCTYNoBHg6/Hwv0N/i+V94MvkOSor6oI=\ngoogle.golang.org/api v0.256.0/go.mod h1:KIgPhksXADEKJlnEoRa9qAII4rXcy40vfI8HRqcU964=\ngoogle.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=\ngoogle.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=\ngoogle.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=\ngoogle.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=\ngoogle.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=\ngoogle.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=\ngoogle.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=\ngoogle.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=\ngoogle.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=\ngoogle.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=\ngoogle.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=\ngoogle.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=\ngoogle.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=\ngoogle.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=\ngoogle.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=\ngoogle.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=\ngoogle.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=\ngoogle.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=\ngoogle.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24=\ngoogle.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k=\ngoogle.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=\ngoogle.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48=\ngoogle.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w=\ngoogle.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=\ngoogle.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=\ngoogle.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc=\ngoogle.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba h1:B14OtaXuMaCQsl2deSvNkyPKIzq3BjfxQp8d00QyWx4=\ngoogle.golang.org/genproto/googleapis/api v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:G5IanEx8/PgI9w6CFcYQf7jMtHQhZruvfM1i3qOqk5U=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba h1:UKgtfRM7Yh93Sya0Fo8ZzhDP4qBckrrxEr2oF5UIVb8=\ngoogle.golang.org/genproto/googleapis/rpc v0.0.0-20251111163417-95abcf5c77ba/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=\ngoogle.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=\ngoogle.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=\ngoogle.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=\ngoogle.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=\ngoogle.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=\ngoogle.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=\ngoogle.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=\ngoogle.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=\ngoogle.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=\ngoogle.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=\ngoogle.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=\ngoogle.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=\ngoogle.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=\ngoogle.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=\ngoogle.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE=\ngoogle.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=\ngoogle.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=\ngoogle.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=\ngoogle.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=\ngoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngoogle.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=\ngoogle.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=\ngoogle.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=\ngoogle.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=\ngoogle.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=\ngoogle.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=\ngopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=\ngopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=\ngopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=\ngopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=\ngopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=\ngopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=\ngorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=\ngotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=\ngotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g=\nhelm.sh/helm/v3 v3.20.0 h1:2M+0qQwnbI1a2CxN7dbmfsWHg/MloeaFMnZCY56as50=\nhelm.sh/helm/v3 v3.20.0/go.mod h1:rTavWa0lagZOxGfdhu4vgk1OjH2UYCnrDKE2PVC4N0o=\nhonnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=\nhonnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=\nhonnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nhonnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=\nk8s.io/api v0.35.2 h1:tW7mWc2RpxW7HS4CoRXhtYHSzme1PN1UjGHJ1bdrtdw=\nk8s.io/api v0.35.2/go.mod h1:7AJfqGoAZcwSFhOjcGM7WV05QxMMgUaChNfLTXDRE60=\nk8s.io/apiextensions-apiserver v0.35.2 h1:iyStXHoJZsUXPh/nFAsjC29rjJWdSgUmG1XpApE29c0=\nk8s.io/apiextensions-apiserver v0.35.2/go.mod h1:OdyGvcO1FtMDWQ+rRh/Ei3b6X3g2+ZDHd0MSRGeS8rU=\nk8s.io/apimachinery v0.35.2 h1:NqsM/mmZA7sHW02JZ9RTtk3wInRgbVxL8MPfzSANAK8=\nk8s.io/apimachinery v0.35.2/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns=\nk8s.io/apiserver v0.35.2 h1:rb52v0CZGEL0FkhjS+I6jHflAp7fZ4MIaKcEHX7wmDk=\nk8s.io/apiserver v0.35.2/go.mod h1:CROJUAu0tfjZLyYgSeBsBan2T7LUJGh0ucWwTCSSk7g=\nk8s.io/cli-runtime v0.35.1 h1:uKcXFe8J7AMAM4Gm2JDK4mp198dBEq2nyeYtO+JfGJE=\nk8s.io/cli-runtime v0.35.1/go.mod h1:55/hiXIq1C8qIJ3WBrWxEwDLdHQYhBNRdZOz9f7yvTw=\nk8s.io/client-go v0.35.2 h1:YUfPefdGJA4aljDdayAXkc98DnPkIetMl4PrKX97W9o=\nk8s.io/client-go v0.35.2/go.mod h1:4QqEwh4oQpeK8AaefZ0jwTFJw/9kIjdQi0jpKeYvz7g=\nk8s.io/component-base v0.35.2 h1:btgR+qNrpWuRSuvWSnQYsZy88yf5gVwemvz0yw79pGc=\nk8s.io/component-base v0.35.2/go.mod h1:B1iBJjooe6xIJYUucAxb26RwhAjzx0gHnqO9htWIX+0=\nk8s.io/component-helpers v0.35.0 h1:wcXv7HJRksgVjM4VlXJ1CNFBpyDHruRI99RrBtrJceA=\nk8s.io/component-helpers v0.35.0/go.mod h1:ahX0m/LTYmu7fL3W8zYiIwnQ/5gT28Ex4o2pymF63Co=\nk8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=\nk8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=\nk8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE=\nk8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ=\nk8s.io/kubectl v0.35.0 h1:cL/wJKHDe8E8+rP3G7avnymcMg6bH6JEcR5w5uo06wc=\nk8s.io/kubectl v0.35.0/go.mod h1:VR5/TSkYyxZwrRwY5I5dDq6l5KXmiCb+9w8IKplk3Qo=\nk8s.io/metrics v0.35.2 h1:PJRP88qeadR5evg4ZKJAh3NR3ICchwM51/Aidd0LHjc=\nk8s.io/metrics v0.35.2/go.mod h1:w1pJmSu2j8ftVI26MGcJtMnpmZ06oKwb4Enm+xVl06Q=\nk8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=\nk8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=\nmodernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=\nmodernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=\nmodernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=\nmodernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=\nmodernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=\nmodernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=\nmodernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=\nmodernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=\nmodernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=\nmodernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=\nmodernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=\nmodernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=\nmodernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=\nmodernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=\nmodernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=\nmodernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=\nmodernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=\nmodernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=\nmodernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=\nmodernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=\nmodernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=\nmodernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=\nmodernc.org/sqlite v1.44.3 h1:+39JvV/HWMcYslAwRxHb8067w+2zowvFOUrOWIy9PjY=\nmodernc.org/sqlite v1.44.3/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=\nmodernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=\nmodernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=\nmodernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=\nmodernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=\noras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc=\noras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o=\nrsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=\nrsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=\nrsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=\nsigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=\nsigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=\nsigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I=\nsigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM=\nsigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78=\nsigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po=\nsigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=\nsigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=\nsigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=\nsigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=\nsigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=\nsigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=\n"
  },
  {
    "path": "internal/client/client.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage client\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/slogs\"\n\tauthorizationv1 \"k8s.io/api/authorization/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/util/cache\"\n\t\"k8s.io/apimachinery/pkg/version\"\n\t\"k8s.io/client-go/discovery/cached/disk\"\n\t\"k8s.io/client-go/dynamic\"\n\t\"k8s.io/client-go/kubernetes\"\n\trestclient \"k8s.io/client-go/rest\"\n\tmetricsapi \"k8s.io/metrics/pkg/apis/metrics\"\n\t\"k8s.io/metrics/pkg/client/clientset/versioned\"\n)\n\nconst (\n\tcacheSize     = 100\n\tcacheExpiry   = 5 * time.Minute\n\tcacheMXAPIKey = \"metricsAPI\"\n\tserverVersion = \"serverVersion\"\n\tcacheNSKey    = \"validNamespaces\"\n)\n\nvar supportedMetricsAPIVersions = []string{\"v1beta1\"}\n\n// NamespaceNames tracks a collection of namespace names.\ntype NamespaceNames map[string]struct{}\n\n// APIClient represents a Kubernetes api client.\ntype APIClient struct {\n\tclient, logClient kubernetes.Interface\n\tdClient           dynamic.Interface\n\tnsClient          dynamic.NamespaceableResourceInterface\n\tmxsClient         *versioned.Clientset\n\tcachedClient      *disk.CachedDiscoveryClient\n\tconfig            *Config\n\tmx                sync.RWMutex\n\tcache             *cache.LRUExpireCache\n\tconnOK            bool\n\tlog               *slog.Logger\n}\n\n// NewTestAPIClient for testing ONLY!!\nfunc NewTestAPIClient() *APIClient {\n\treturn &APIClient{\n\t\tconfig: NewConfig(nil),\n\t\tcache:  cache.NewLRUExpireCache(cacheSize),\n\t}\n}\n\n// InitConnection initialize connection from command line args.\n// Checks for connectivity with the api server.\nfunc InitConnection(config *Config, log *slog.Logger) (*APIClient, error) {\n\ta := APIClient{\n\t\tconfig: config,\n\t\tcache:  cache.NewLRUExpireCache(cacheSize),\n\t\tconnOK: true,\n\t\tlog:    log.With(slogs.Subsys, \"client\"),\n\t}\n\tif err := a.supportsMetricsResources(); err != nil {\n\t\tslog.Warn(\"Fail to locate metrics-server\", slogs.Error, err)\n\t\tif !errors.Is(err, noMetricServerErr) && !errors.Is(err, metricsUnsupportedErr) {\n\t\t\ta.connOK = false\n\t\t\treturn &a, err\n\t\t}\n\t}\n\treturn &a, nil\n}\n\n// ConnectionOK returns connection status.\nfunc (a *APIClient) ConnectionOK() bool {\n\treturn a.connOK\n}\n\nfunc makeSAR(ns string, gvr *GVR, name string) *authorizationv1.SelfSubjectAccessReview {\n\tif ns == ClusterScope {\n\t\tns = BlankNamespace\n\t}\n\tres := gvr.GVR()\n\treturn &authorizationv1.SelfSubjectAccessReview{\n\t\tSpec: authorizationv1.SelfSubjectAccessReviewSpec{\n\t\t\tResourceAttributes: &authorizationv1.ResourceAttributes{\n\t\t\t\tNamespace:   ns,\n\t\t\t\tGroup:       res.Group,\n\t\t\t\tVersion:     res.Version,\n\t\t\t\tResource:    res.Resource,\n\t\t\t\tSubresource: gvr.SubResource(),\n\t\t\t\tName:        name,\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc makeCacheKey(ns string, gvr *GVR, n string, vv []string) string {\n\treturn ns + \":\" + gvr.String() + \":\" + n + \"::\" + strings.Join(vv, \",\")\n}\n\n// ActiveContext returns the current context name.\nfunc (a *APIClient) ActiveContext() string {\n\tc, err := a.config.CurrentContextName()\n\tif err != nil {\n\t\tslog.Error(\"unable to located active cluster\", slogs.Error, err)\n\t\treturn \"\"\n\t}\n\treturn c\n}\n\n// IsActiveNamespace returns true if namespaces matches.\nfunc (a *APIClient) IsActiveNamespace(ns string) bool {\n\tif a.ActiveNamespace() == BlankNamespace {\n\t\treturn true\n\t}\n\n\treturn a.ActiveNamespace() == ns\n}\n\n// ActiveNamespace returns the current namespace.\nfunc (a *APIClient) ActiveNamespace() string {\n\tif ns, err := a.CurrentNamespaceName(); err == nil {\n\t\treturn ns\n\t}\n\n\treturn BlankNamespace\n}\n\nfunc (a *APIClient) clearCache() {\n\tfor _, k := range a.cache.Keys() {\n\t\ta.cache.Remove(k)\n\t}\n}\n\n// CanI checks if user has access to a certain resource.\nfunc (a *APIClient) CanI(ns string, gvr *GVR, name string, verbs []string) (auth bool, err error) {\n\tif !a.getConnOK() {\n\t\treturn false, errors.New(\"ACCESS -- No API server connection\")\n\t}\n\tif gvr == NsGVR {\n\t\t// The name of the namespace is required to check permissions in some cases\n\t\tns = name\n\t}\n\tif IsClusterWide(ns) {\n\t\tns = BlankNamespace\n\t}\n\tif gvr == HmGVR {\n\t\t// helm stores release data in secrets\n\t\tgvr = SecGVR\n\t}\n\tkey := makeCacheKey(ns, gvr, name, verbs)\n\tif v, ok := a.cache.Get(key); ok {\n\t\tif auth, ok = v.(bool); ok {\n\t\t\treturn auth, nil\n\t\t}\n\t}\n\n\tclog := a.log.With(slogs.Subsys, \"can\")\n\n\tdial, err := a.Dial()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tclient, sar := dial.AuthorizationV1().SelfSubjectAccessReviews(), makeSAR(ns, gvr, name)\n\n\tctx, cancel := context.WithTimeout(context.Background(), a.config.CallTimeout())\n\tdefer cancel()\n\tfor _, v := range verbs {\n\t\tsar.Spec.ResourceAttributes.Verb = v\n\t\tresp, err := client.Create(ctx, sar, metav1.CreateOptions{})\n\t\tclog.Debug(\"[CAN] access\",\n\t\t\tslogs.GVR, gvr,\n\t\t\tslogs.Namespace, ns,\n\t\t\tslogs.ResName, name,\n\t\t\tslogs.Verb, verbs,\n\t\t)\n\t\tif resp != nil {\n\t\t\tclog.Debug(\"[CAN] response\",\n\t\t\t\tslogs.AuthStatus, resp.Status.Allowed,\n\t\t\t\tslogs.AuthReason, resp.Status.Reason,\n\t\t\t)\n\t\t}\n\t\tif err != nil {\n\t\t\tclog.Warn(\"Auth request failed\", slogs.Error, err)\n\t\t\ta.cache.Add(key, false, cacheExpiry)\n\t\t\treturn auth, err\n\t\t}\n\t\tif !resp.Status.Allowed {\n\t\t\ta.cache.Add(key, false, cacheExpiry)\n\t\t\treturn auth, fmt.Errorf(\"(%s) access denied for user on resource %q:%s in namespace %q\", v, name, gvr, ns)\n\t\t}\n\t}\n\tauth = true\n\ta.cache.Add(key, true, cacheExpiry)\n\n\treturn\n}\n\n// CurrentNamespaceName return namespace name set via either cli arg or cluster config.\nfunc (a *APIClient) CurrentNamespaceName() (string, error) {\n\treturn a.config.CurrentNamespaceName()\n}\n\n// ServerVersion returns the current server version info.\nfunc (a *APIClient) ServerVersion() (*version.Info, error) {\n\tif v, ok := a.cache.Get(serverVersion); ok {\n\t\tif vi, ok := v.(*version.Info); ok {\n\t\t\treturn vi, nil\n\t\t}\n\t}\n\tdial, err := a.CachedDiscovery()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tinfo, err := dial.ServerVersion()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ta.cache.Add(serverVersion, info, cacheExpiry)\n\n\treturn info, nil\n}\n\nfunc (a *APIClient) IsValidNamespace(ns string) bool {\n\tok, err := a.isValidNamespace(ns)\n\tif err != nil {\n\t\tslog.Warn(\"Namespace validation failed\",\n\t\t\tslogs.Namespace, ns,\n\t\t\tslogs.Error, err,\n\t\t)\n\t}\n\n\treturn ok\n}\n\nfunc (a *APIClient) isValidNamespace(n string) (bool, error) {\n\tif IsClusterWide(n) || n == NotNamespaced {\n\t\treturn true, nil\n\t}\n\tnn, err := a.ValidNamespaceNames()\n\tif err != nil {\n\t\treturn false, err\n\t}\n\t_, ok := nn[n]\n\n\treturn ok, nil\n}\n\n// ValidNamespaceNames returns all available namespaces.\nfunc (a *APIClient) ValidNamespaceNames() (NamespaceNames, error) {\n\tif a == nil {\n\t\treturn nil, fmt.Errorf(\"validNamespaces: no available client found\")\n\t}\n\n\tif nn, ok := a.cache.Get(cacheNSKey); ok {\n\t\tif nss, ok := nn.(NamespaceNames); ok {\n\t\t\treturn nss, nil\n\t\t}\n\t}\n\n\tok, err := a.CanI(ClusterScope, NsGVR, \"\", ListAccess)\n\tif !ok || err != nil {\n\t\ta.cache.Add(cacheNSKey, NamespaceNames{}, cacheExpiry)\n\t\treturn nil, fmt.Errorf(\"user not authorized to list all namespaces\")\n\t}\n\n\tdial, err := a.Dial()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), a.config.CallTimeout())\n\tdefer cancel()\n\tnn, err := dial.CoreV1().Namespaces().List(ctx, metav1.ListOptions{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tnns := make(NamespaceNames, len(nn.Items))\n\tfor i := range nn.Items {\n\t\tnns[nn.Items[i].Name] = struct{}{}\n\t}\n\ta.cache.Add(cacheNSKey, nns, cacheExpiry)\n\n\treturn nns, nil\n}\n\n// CheckConnectivity return true if api server is cool or false otherwise.\nfunc (a *APIClient) CheckConnectivity() bool {\n\tdefer func() {\n\t\tif err := recover(); err != nil {\n\t\t\ta.setConnOK(false)\n\t\t}\n\t\tif !a.getConnOK() {\n\t\t\ta.clearCache()\n\t\t}\n\t}()\n\n\tcfg, err := a.config.RESTConfig()\n\tif err != nil {\n\t\tslog.Error(\"RestConfig load failed\", slogs.Error, err)\n\t\ta.connOK = false\n\t\treturn a.connOK\n\t}\n\tcfg.Timeout = a.config.CallTimeout()\n\tclient, err := kubernetes.NewForConfig(cfg)\n\tif err != nil {\n\t\tslog.Error(\"Unable to connect to api server\", slogs.Error, err)\n\t\ta.setConnOK(false)\n\t\treturn a.getConnOK()\n\t}\n\n\tif _, err := client.ServerVersion(); err == nil {\n\t\ta.setClient(client)\n\t\tif !a.getConnOK() {\n\t\t\ta.reset()\n\t\t}\n\t} else {\n\t\tslog.Error(\"Unable to fetch server version\", slogs.Error, err)\n\t\ta.setConnOK(false)\n\t}\n\n\treturn a.getConnOK()\n}\n\n// Config return a kubernetes configuration.\nfunc (a *APIClient) Config() *Config {\n\treturn a.config\n}\n\n// HasMetrics checks if the cluster supports metrics.\nfunc (a *APIClient) HasMetrics() bool {\n\treturn a.supportsMetricsResources() == nil\n}\n\nfunc (a *APIClient) getMxsClient() *versioned.Clientset {\n\ta.mx.RLock()\n\tdefer a.mx.RUnlock()\n\n\treturn a.mxsClient\n}\n\nfunc (a *APIClient) setMxsClient(c *versioned.Clientset) {\n\ta.mx.Lock()\n\tdefer a.mx.Unlock()\n\n\ta.mxsClient = c\n}\n\nfunc (a *APIClient) getCachedClient() *disk.CachedDiscoveryClient {\n\ta.mx.RLock()\n\tdefer a.mx.RUnlock()\n\n\treturn a.cachedClient\n}\n\nfunc (a *APIClient) setCachedClient(c *disk.CachedDiscoveryClient) {\n\ta.mx.Lock()\n\tdefer a.mx.Unlock()\n\n\ta.cachedClient = c\n}\n\nfunc (a *APIClient) getDClient() dynamic.Interface {\n\ta.mx.RLock()\n\tdefer a.mx.RUnlock()\n\n\treturn a.dClient\n}\n\nfunc (a *APIClient) setDClient(c dynamic.Interface) {\n\ta.mx.Lock()\n\tdefer a.mx.Unlock()\n\n\ta.dClient = c\n}\n\nfunc (a *APIClient) getConnOK() bool {\n\ta.mx.RLock()\n\tdefer a.mx.RUnlock()\n\n\treturn a.connOK\n}\n\nfunc (a *APIClient) setConnOK(b bool) {\n\ta.mx.Lock()\n\tdefer a.mx.Unlock()\n\n\ta.connOK = b\n}\n\nfunc (a *APIClient) setLogClient(k kubernetes.Interface) {\n\ta.mx.Lock()\n\tdefer a.mx.Unlock()\n\n\ta.logClient = k\n}\n\nfunc (a *APIClient) getLogClient() kubernetes.Interface {\n\ta.mx.RLock()\n\tdefer a.mx.RUnlock()\n\n\treturn a.logClient\n}\n\nfunc (a *APIClient) setClient(k kubernetes.Interface) {\n\ta.mx.Lock()\n\tdefer a.mx.Unlock()\n\n\ta.client = k\n}\n\nfunc (a *APIClient) getClient() kubernetes.Interface {\n\ta.mx.RLock()\n\tdefer a.mx.RUnlock()\n\n\treturn a.client\n}\n\n// DialLogs returns a handle to api server for logs.\nfunc (a *APIClient) DialLogs() (kubernetes.Interface, error) {\n\tif !a.getConnOK() {\n\t\treturn nil, errors.New(\"dialLogs - no connection to dial\")\n\t}\n\tif clt := a.getLogClient(); clt != nil {\n\t\treturn clt, nil\n\t}\n\n\tcfg, err := a.RestConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcfg.Timeout = 0\n\tc, err := kubernetes.NewForConfig(cfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ta.setLogClient(c)\n\n\treturn a.getLogClient(), nil\n}\n\n// Dial returns a handle to api server or die.\nfunc (a *APIClient) Dial() (kubernetes.Interface, error) {\n\tif !a.getConnOK() {\n\t\treturn nil, errors.New(\"no connection to dial\")\n\t}\n\tif c := a.getClient(); c != nil {\n\t\treturn c, nil\n\t}\n\n\tcfg, err := a.RestConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tc, err := kubernetes.NewForConfig(cfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ta.setClient(c)\n\n\treturn a.getClient(), nil\n}\n\n// RestConfig returns a rest api client.\nfunc (a *APIClient) RestConfig() (*restclient.Config, error) {\n\treturn a.config.RESTConfig()\n}\n\n// CachedDiscovery returns a cached discovery client.\nfunc (a *APIClient) CachedDiscovery() (*disk.CachedDiscoveryClient, error) {\n\tif !a.getConnOK() {\n\t\treturn nil, errors.New(\"no connection to cached dial\")\n\t}\n\n\tif c := a.getCachedClient(); c != nil {\n\t\treturn c, nil\n\t}\n\n\tcfg, err := a.RestConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbaseCacheDir := os.Getenv(\"KUBECACHEDIR\")\n\tif baseCacheDir == \"\" {\n\t\tbaseCacheDir = filepath.Join(mustHomeDir(), \".kube\", \"cache\")\n\t}\n\n\thttpCacheDir := filepath.Join(baseCacheDir, \"http\")\n\tdiscCacheDir := filepath.Join(baseCacheDir, \"discovery\", toHostDir(cfg.Host))\n\n\tc, err := disk.NewCachedDiscoveryClientForConfig(cfg, discCacheDir, httpCacheDir, cacheExpiry)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ta.setCachedClient(c)\n\n\treturn a.getCachedClient(), nil\n}\n\n// DynDial returns a handle to a dynamic interface.\nfunc (a *APIClient) DynDial() (dynamic.Interface, error) {\n\tif c := a.getDClient(); c != nil {\n\t\treturn c, nil\n\t}\n\n\tcfg, err := a.RestConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tc, err := dynamic.NewForConfig(cfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ta.setDClient(c)\n\n\treturn a.getDClient(), nil\n}\n\n// MXDial returns a handle to the metrics server.\nfunc (a *APIClient) MXDial() (*versioned.Clientset, error) {\n\tif c := a.getMxsClient(); c != nil {\n\t\treturn c, nil\n\t}\n\n\tcfg, err := a.RestConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc, err := versioned.NewForConfig(cfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ta.setMxsClient(c)\n\n\treturn a.getMxsClient(), err\n}\n\nfunc (a *APIClient) invalidateCache() error {\n\tdial, err := a.CachedDiscovery()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdial.Invalidate()\n\n\treturn nil\n}\n\n// SwitchContext handles kubeconfig context switches.\nfunc (a *APIClient) SwitchContext(name string) error {\n\tslog.Debug(\"Switching context\", slogs.Context, name)\n\tif err := a.config.SwitchContext(name); err != nil {\n\t\treturn err\n\t}\n\ta.reset()\n\tResetMetrics()\n\ta.config = NewConfig(a.config.flags)\n\tif !a.CheckConnectivity() {\n\t\tslog.Warn(\"SwitchContext: connectivity check failed\", slogs.Context, name)\n\t}\n\n\tif _, err := a.DynDial(); err != nil {\n\t\tslog.Warn(\"SwitchContext: DynDial pre-warm failed\", slogs.Error, err)\n\t}\n\treturn a.invalidateCache()\n}\n\nfunc (a *APIClient) reset() {\n\ta.config.reset()\n\ta.cache = cache.NewLRUExpireCache(cacheSize)\n\ta.nsClient = nil\n\n\ta.setDClient(nil)\n\ta.setMxsClient(nil)\n\ta.setCachedClient(nil)\n\ta.setClient(nil)\n\ta.setLogClient(nil)\n\ta.setConnOK(true)\n}\n\nfunc (a *APIClient) checkCacheBool(key string) (state, ok bool) {\n\tv, found := a.cache.Get(key)\n\tif !found {\n\t\treturn\n\t}\n\tstate, ok = v.(bool)\n\treturn\n}\n\nfunc (a *APIClient) supportsMetricsResources() error {\n\tsupported, ok := a.checkCacheBool(cacheMXAPIKey)\n\tif ok {\n\t\tif supported {\n\t\t\treturn nil\n\t\t}\n\t\treturn noMetricServerErr\n\t}\n\n\tdefer func() {\n\t\ta.cache.Add(cacheMXAPIKey, supported, cacheExpiry)\n\t}()\n\n\tdial, err := a.Dial()\n\tif err != nil {\n\t\tslog.Warn(\"Unable to dial API client for metrics\", slogs.Error, err)\n\t\treturn err\n\t}\n\tapiGroups, err := dial.Discovery().ServerGroups()\n\tif err != nil {\n\t\treturn err\n\t}\n\tfor i := range apiGroups.Groups {\n\t\tif apiGroups.Groups[i].Name != metricsapi.GroupName {\n\t\t\tcontinue\n\t\t}\n\t\tif checkMetricsVersion(&(apiGroups.Groups[i])) {\n\t\t\tsupported = true\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn metricsUnsupportedErr\n}\n\nfunc checkMetricsVersion(grp *metav1.APIGroup) bool {\n\tfor _, v := range grp.Versions {\n\t\tfor _, supportedVersion := range supportedMetricsAPIVersions {\n\t\t\tif v.Version == supportedVersion {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "internal/client/client_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage client\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/assert\"\n\tauthorizationv1 \"k8s.io/api/authorization/v1\"\n)\n\nfunc TestMakeSAR(t *testing.T) {\n\tuu := map[string]struct {\n\t\tns  string\n\t\tgvr *GVR\n\t\tsar *authorizationv1.SelfSubjectAccessReview\n\t}{\n\t\t\"all-pods\": {\n\t\t\tns:  NamespaceAll,\n\t\t\tgvr: PodGVR,\n\t\t\tsar: &authorizationv1.SelfSubjectAccessReview{\n\t\t\t\tSpec: authorizationv1.SelfSubjectAccessReviewSpec{\n\t\t\t\t\tResourceAttributes: &authorizationv1.ResourceAttributes{\n\t\t\t\t\t\tNamespace: NamespaceAll,\n\t\t\t\t\t\tVersion:   \"v1\",\n\t\t\t\t\t\tResource:  \"pods\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"ns-pods\": {\n\t\t\tns:  \"fred\",\n\t\t\tgvr: PodGVR,\n\t\t\tsar: &authorizationv1.SelfSubjectAccessReview{\n\t\t\t\tSpec: authorizationv1.SelfSubjectAccessReviewSpec{\n\t\t\t\t\tResourceAttributes: &authorizationv1.ResourceAttributes{\n\t\t\t\t\t\tNamespace: \"fred\",\n\t\t\t\t\t\tVersion:   \"v1\",\n\t\t\t\t\t\tResource:  \"pods\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"clusterscope-ns\": {\n\t\t\tns:  ClusterScope,\n\t\t\tgvr: NsGVR,\n\t\t\tsar: &authorizationv1.SelfSubjectAccessReview{\n\t\t\t\tSpec: authorizationv1.SelfSubjectAccessReviewSpec{\n\t\t\t\t\tResourceAttributes: &authorizationv1.ResourceAttributes{\n\t\t\t\t\t\tVersion:  \"v1\",\n\t\t\t\t\t\tResource: \"namespaces\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"subres-pods\": {\n\t\t\tns:  \"fred\",\n\t\t\tgvr: NewGVR(\"v1/pods:logs\"),\n\t\t\tsar: &authorizationv1.SelfSubjectAccessReview{\n\t\t\t\tSpec: authorizationv1.SelfSubjectAccessReviewSpec{\n\t\t\t\t\tResourceAttributes: &authorizationv1.ResourceAttributes{\n\t\t\t\t\t\tNamespace:   \"fred\",\n\t\t\t\t\t\tVersion:     \"v1\",\n\t\t\t\t\t\tResource:    \"pods\",\n\t\t\t\t\t\tSubresource: \"logs\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.sar, makeSAR(u.ns, u.gvr, \"\"))\n\t\t})\n\t}\n}\n\nfunc TestIsValidNamespace(t *testing.T) {\n\tc := NewTestAPIClient()\n\n\tuu := map[string]struct {\n\t\tns    string\n\t\tcache NamespaceNames\n\t\tok    bool\n\t}{\n\t\t\"all-ns\": {\n\t\t\tns: NamespaceAll,\n\t\t\tcache: NamespaceNames{\n\t\t\t\tDefaultNamespace: {},\n\t\t\t},\n\t\t\tok: true,\n\t\t},\n\t\t\"blank-ns\": {\n\t\t\tns: BlankNamespace,\n\t\t\tcache: NamespaceNames{\n\t\t\t\tDefaultNamespace: {},\n\t\t\t},\n\t\t\tok: true,\n\t\t},\n\t\t\"cluster-ns\": {\n\t\t\tns: ClusterScope,\n\t\t\tcache: NamespaceNames{\n\t\t\t\tDefaultNamespace: {},\n\t\t\t},\n\t\t\tok: true,\n\t\t},\n\t\t\"no-ns\": {\n\t\t\tns: NotNamespaced,\n\t\t\tcache: NamespaceNames{\n\t\t\t\tDefaultNamespace: {},\n\t\t\t},\n\t\t\tok: true,\n\t\t},\n\t\t\"default-ns\": {\n\t\t\tns: DefaultNamespace,\n\t\t\tcache: NamespaceNames{\n\t\t\t\tDefaultNamespace: {},\n\t\t\t},\n\t\t\tok: true,\n\t\t},\n\t\t\"valid-ns\": {\n\t\t\tns: \"fred\",\n\t\t\tcache: NamespaceNames{\n\t\t\t\t\"fred\": {},\n\t\t\t},\n\t\t\tok: true,\n\t\t},\n\t\t\"invalid-ns\": {\n\t\t\tns: \"fred\",\n\t\t\tcache: NamespaceNames{\n\t\t\t\tDefaultNamespace: {},\n\t\t\t},\n\t\t},\n\t}\n\n\texpiry := 1 * time.Millisecond\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tc.cache.Add(\"validNamespaces\", u.cache, expiry)\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.ok, c.IsValidNamespace(u.ns))\n\t\t})\n\t}\n}\n\nfunc TestCheckCacheBool(t *testing.T) {\n\tc := NewTestAPIClient()\n\n\tconst key = \"fred\"\n\tuu := map[string]struct {\n\t\tkey                  string\n\t\tval                  any\n\t\tfound, actual, sleep bool\n\t}{\n\t\t\"setTrue\": {\n\t\t\tkey:    key,\n\t\t\tval:    true,\n\t\t\tfound:  true,\n\t\t\tactual: true,\n\t\t},\n\t\t\"setFalse\": {\n\t\t\tkey:   key,\n\t\t\tval:   false,\n\t\t\tfound: true,\n\t\t},\n\t\t\"missing\": {\n\t\t\tkey: \"blah\",\n\t\t\tval: false,\n\t\t},\n\t\t\"expired\": {\n\t\t\tkey:   key,\n\t\t\tval:   true,\n\t\t\tsleep: true,\n\t\t},\n\t}\n\n\texpiry := 1 * time.Millisecond\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tc.cache.Add(key, u.val, expiry)\n\t\tif u.sleep {\n\t\t\ttime.Sleep(expiry)\n\t\t}\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tval, ok := c.checkCacheBool(u.key)\n\t\t\tassert.Equal(t, u.found, ok)\n\t\t\tassert.Equal(t, u.actual, val)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/client/config.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage client\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"k8s.io/cli-runtime/pkg/genericclioptions\"\n\trestclient \"k8s.io/client-go/rest\"\n\t\"k8s.io/client-go/tools/clientcmd\"\n\t\"k8s.io/client-go/tools/clientcmd/api\"\n)\n\nconst (\n\t// DefaultCallTimeoutDuration is the default api server call timeout duration.\n\tDefaultCallTimeoutDuration time.Duration = 120 * time.Second\n\n\t// UsePersistentConfig caches client config to avoid reloads.\n\tUsePersistentConfig = true\n)\n\n// Config tracks a kubernetes configuration.\ntype Config struct {\n\tflags *genericclioptions.ConfigFlags\n\tmx    sync.RWMutex\n\tproxy func(*http.Request) (*url.URL, error)\n}\n\n// NewConfig returns a new k8s config or an error if the flags are invalid.\nfunc NewConfig(f *genericclioptions.ConfigFlags) *Config {\n\treturn &Config{\n\t\tflags: f,\n\t}\n}\n\n// CallTimeout returns the call timeout if set or the default if not set.\nfunc (c *Config) CallTimeout() time.Duration {\n\tif !isSet(c.flags.Timeout) {\n\t\treturn DefaultCallTimeoutDuration\n\t}\n\tdur, err := time.ParseDuration(*c.flags.Timeout)\n\tif err != nil {\n\t\treturn DefaultCallTimeoutDuration\n\t}\n\n\treturn dur\n}\n\nfunc (c *Config) RESTConfig() (*restclient.Config, error) {\n\tcfg, err := c.clientConfig().ClientConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif c.proxy != nil {\n\t\tcfg.Proxy = c.proxy\n\t}\n\n\treturn cfg, nil\n}\n\n// Flags returns configuration flags.\nfunc (c *Config) Flags() *genericclioptions.ConfigFlags {\n\treturn c.flags\n}\n\nfunc (c *Config) RawConfig() (api.Config, error) {\n\treturn c.clientConfig().RawConfig()\n}\n\nfunc (c *Config) clientConfig() clientcmd.ClientConfig {\n\treturn c.flags.ToRawKubeConfigLoader()\n}\n\nfunc (*Config) reset() {}\n\n// SwitchContext changes the kubeconfig context to a new cluster.\nfunc (c *Config) SwitchContext(name string) error {\n\tct, err := c.GetContext(name)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"context %q does not exist\", name)\n\t}\n\t// !!BOZO!! Do you need to reset the flags?\n\tflags := genericclioptions.NewConfigFlags(UsePersistentConfig)\n\tflags.Context, flags.ClusterName = &name, &ct.Cluster\n\tflags.Namespace = c.flags.Namespace\n\tflags.Timeout = c.flags.Timeout\n\tflags.KubeConfig = c.flags.KubeConfig\n\tflags.Impersonate = c.flags.Impersonate\n\tflags.ImpersonateGroup = c.flags.ImpersonateGroup\n\tflags.ImpersonateUID = c.flags.ImpersonateUID\n\tflags.Insecure = c.flags.Insecure\n\tflags.BearerToken = c.flags.BearerToken\n\n\tc.flags = flags\n\n\treturn nil\n}\n\nfunc (c *Config) Clone(ns string) (*genericclioptions.ConfigFlags, error) {\n\tflags := genericclioptions.NewConfigFlags(false)\n\tct, err := c.CurrentContextName()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcl, err := c.CurrentClusterName()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tflags.Context, flags.ClusterName = &ct, &cl\n\tflags.Namespace = &ns\n\tflags.Timeout = c.Flags().Timeout\n\tflags.KubeConfig = c.Flags().KubeConfig\n\n\treturn flags, nil\n}\n\n// CurrentClusterName returns the currently active cluster name.\nfunc (c *Config) CurrentClusterName() (string, error) {\n\tif isSet(c.flags.ClusterName) {\n\t\treturn *c.flags.ClusterName, nil\n\t}\n\tcfg, err := c.RawConfig()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tct, ok := cfg.Contexts[cfg.CurrentContext]\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"invalid current context specified: %q\", cfg.CurrentContext)\n\t}\n\tif isSet(c.flags.Context) {\n\t\tct, ok = cfg.Contexts[*c.flags.Context]\n\t\tif !ok {\n\t\t\treturn \"\", fmt.Errorf(\"current-cluster - invalid context specified: %q\", *c.flags.Context)\n\t\t}\n\t}\n\n\treturn ct.Cluster, nil\n}\n\n// CurrentContextName returns the currently active config context.\nfunc (c *Config) CurrentContextName() (string, error) {\n\tif isSet(c.flags.Context) {\n\t\treturn *c.flags.Context, nil\n\t}\n\tcfg, err := c.RawConfig()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"fail to load rawConfig: %w\", err)\n\t}\n\n\treturn cfg.CurrentContext, nil\n}\n\nfunc (c *Config) CurrentContextNamespace() (string, error) {\n\tname, err := c.CurrentContextName()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tcontext, err := c.GetContext(name)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn context.Namespace, nil\n}\n\n// CurrentContext returns the current context configuration.\nfunc (c *Config) CurrentContext() (*api.Context, error) {\n\tn, err := c.CurrentContextName()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn c.GetContext(n)\n}\n\n// GetContext fetch a given context or error if it does not exist.\nfunc (c *Config) GetContext(n string) (*api.Context, error) {\n\tcfg, err := c.RawConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif c, ok := cfg.Contexts[n]; ok {\n\t\treturn c, nil\n\t}\n\n\treturn nil, fmt.Errorf(\"getcontext - invalid context specified: %q\", n)\n}\n\n// SetProxy sets the proxy function.\nfunc (c *Config) SetProxy(proxy func(*http.Request) (*url.URL, error)) {\n\tc.proxy = proxy\n}\n\n// Contexts fetch all available contexts.\nfunc (c *Config) Contexts() (map[string]*api.Context, error) {\n\tcfg, err := c.RawConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn cfg.Contexts, nil\n}\n\n// DelContext remove a given context from the configuration.\nfunc (c *Config) DelContext(n string) error {\n\tcfg, err := c.RawConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdelete(cfg.Contexts, n)\n\n\tacc, err := c.ConfigAccess()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn clientcmd.ModifyConfig(acc, cfg, true)\n}\n\n// RenameContext renames a context.\nfunc (c *Config) RenameContext(oldCtx, newCtx string) error {\n\tcfg, err := c.RawConfig()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif _, ok := cfg.Contexts[newCtx]; ok {\n\t\treturn fmt.Errorf(\"context with name %s already exists\", newCtx)\n\t}\n\tcfg.Contexts[newCtx] = cfg.Contexts[oldCtx]\n\tdelete(cfg.Contexts, oldCtx)\n\tacc, err := c.ConfigAccess()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif e := clientcmd.ModifyConfig(acc, cfg, true); e != nil {\n\t\treturn e\n\t}\n\tcurrent, err := c.CurrentContextName()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif current == oldCtx {\n\t\treturn c.SwitchContext(newCtx)\n\t}\n\n\treturn nil\n}\n\n// ContextNames fetch all available contexts.\nfunc (c *Config) ContextNames() (map[string]struct{}, error) {\n\tcfg, err := c.RawConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcc := make(map[string]struct{}, len(cfg.Contexts))\n\tfor n := range cfg.Contexts {\n\t\tcc[n] = struct{}{}\n\t}\n\n\treturn cc, nil\n}\n\n// CurrentGroupNames retrieves the active group names.\nfunc (c *Config) CurrentGroupNames() ([]string, error) {\n\tif areSet(c.flags.ImpersonateGroup) {\n\t\treturn *c.flags.ImpersonateGroup, nil\n\t}\n\n\treturn []string{}, errors.New(\"unable to locate current group\")\n}\n\n// ImpersonateGroups retrieves the active groups if set on the CLI.\nfunc (c *Config) ImpersonateGroups() (string, error) {\n\tif areSet(c.flags.ImpersonateGroup) {\n\t\treturn strings.Join(*c.flags.ImpersonateGroup, \",\"), nil\n\t}\n\n\treturn \"\", errors.New(\"no groups set\")\n}\n\n// ImpersonateUser retrieves the active user name if set on the CLI.\nfunc (c *Config) ImpersonateUser() (string, error) {\n\tif isSet(c.flags.Impersonate) {\n\t\treturn *c.flags.Impersonate, nil\n\t}\n\n\treturn \"\", errors.New(\"no user set\")\n}\n\n// CurrentUserName retrieves the active user name.\nfunc (c *Config) CurrentUserName() (string, error) {\n\tif isSet(c.flags.Impersonate) {\n\t\treturn *c.flags.Impersonate, nil\n\t}\n\n\tif isSet(c.flags.AuthInfoName) {\n\t\treturn *c.flags.AuthInfoName, nil\n\t}\n\n\tcfg, err := c.RawConfig()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tcurrent := cfg.CurrentContext\n\tif isSet(c.flags.Context) {\n\t\tcurrent = *c.flags.Context\n\t}\n\tif ctx, ok := cfg.Contexts[current]; ok {\n\t\treturn ctx.AuthInfo, nil\n\t}\n\n\treturn \"\", errors.New(\"unable to locate current user\")\n}\n\n// CurrentNamespaceName retrieves the active namespace.\nfunc (c *Config) CurrentNamespaceName() (string, error) {\n\tns, overridden, err := c.clientConfig().Namespace()\n\tif err != nil {\n\t\treturn BlankNamespace, err\n\t}\n\t// Checks if ns is passed is in args.\n\tif overridden {\n\t\treturn ns, nil\n\t}\n\n\t// Return ns set in context if any??\n\treturn c.CurrentContextNamespace()\n}\n\n// ConfigAccess return the current kubeconfig api server access configuration.\nfunc (c *Config) ConfigAccess() (clientcmd.ConfigAccess, error) {\n\tc.mx.RLock()\n\tdefer c.mx.RUnlock()\n\n\treturn c.clientConfig().ConfigAccess(), nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc isSet(s *string) bool {\n\treturn s != nil && *s != \"\"\n}\n\nfunc areSet(ss *[]string) bool {\n\treturn ss != nil && len(*ss) != 0\n}\n"
  },
  {
    "path": "internal/client/config_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage client_test\n\nimport (\n\t\"errors\"\n\t\"log/slog\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/cli-runtime/pkg/genericclioptions\"\n)\n\nvar kubeConfig = \"./testdata/config\"\n\nfunc init() {\n\tslog.SetDefault(slog.New(slog.DiscardHandler))\n}\n\nfunc TestCallTimeout(t *testing.T) {\n\tuu := map[string]struct {\n\t\tt string\n\t\te time.Duration\n\t}{\n\t\t\"custom\": {\n\t\t\tt: \"1m\",\n\t\t\te: 1 * time.Minute,\n\t\t},\n\t\t\"default\": {\n\t\t\te: client.DefaultCallTimeoutDuration,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tflags := genericclioptions.NewConfigFlags(false)\n\t\t\tflags.Timeout = &u.t\n\t\t\tcfg := client.NewConfig(flags)\n\t\t\tassert.Equal(t, u.e, cfg.CallTimeout())\n\t\t})\n\t}\n}\n\nfunc TestConfigCurrentContext(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcontext string\n\t\te       string\n\t}{\n\t\t\"default\": {\n\t\t\te: \"fred\",\n\t\t},\n\t\t\"custom\": {\n\t\t\tcontext: \"blee\",\n\t\t\te:       \"blee\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tflags := genericclioptions.NewConfigFlags(false)\n\t\t\tflags.KubeConfig = &kubeConfig\n\t\t\tif u.context != \"\" {\n\t\t\t\tflags.Context = &u.context\n\t\t\t}\n\t\t\tcfg := client.NewConfig(flags)\n\t\t\tctx, err := cfg.CurrentContextName()\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, u.e, ctx)\n\t\t})\n\t}\n}\n\nfunc TestConfigCurrentCluster(t *testing.T) {\n\tname := \"blee\"\n\tuu := map[string]struct {\n\t\tflags   *genericclioptions.ConfigFlags\n\t\tcluster string\n\t}{\n\t\t\"default\": {\n\t\t\tflags: &genericclioptions.ConfigFlags{\n\t\t\t\tKubeConfig: &kubeConfig,\n\t\t\t},\n\t\t\tcluster: \"zorg\",\n\t\t},\n\t\t\"custom\": {\n\t\t\tflags: &genericclioptions.ConfigFlags{\n\t\t\t\tKubeConfig: &kubeConfig,\n\t\t\t\tContext:    &name,\n\t\t\t},\n\t\t\tcluster: \"blee\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tcfg := client.NewConfig(u.flags)\n\t\t\tct, err := cfg.CurrentClusterName()\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, u.cluster, ct)\n\t\t})\n\t}\n}\n\nfunc TestConfigCurrentUser(t *testing.T) {\n\tname := \"blee\"\n\tuu := map[string]struct {\n\t\tflags *genericclioptions.ConfigFlags\n\t\tuser  string\n\t}{\n\t\t\"default\": {\n\t\t\tflags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig},\n\t\t\tuser:  \"fred\",\n\t\t},\n\t\t\"custom\": {\n\t\t\tflags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, AuthInfoName: &name},\n\t\t\tuser:  \"blee\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tcfg := client.NewConfig(u.flags)\n\t\t\tctx, err := cfg.CurrentUserName()\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, u.user, ctx)\n\t\t})\n\t}\n}\n\nfunc TestConfigCurrentNamespace(t *testing.T) {\n\tbleeNS, bleeCTX := \"blee\", \"blee\"\n\tuu := map[string]struct {\n\t\tflags     *genericclioptions.ConfigFlags\n\t\tnamespace string\n\t}{\n\t\t\"default\": {\n\t\t\tflags:     &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig},\n\t\t\tnamespace: \"\",\n\t\t},\n\t\t\"withContext\": {\n\t\t\tflags:     &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, Context: &bleeCTX},\n\t\t\tnamespace: \"zorg\",\n\t\t},\n\t\t\"withNS\": {\n\t\t\tflags:     &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, Namespace: &bleeNS},\n\t\t\tnamespace: \"blee\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tcfg := client.NewConfig(u.flags)\n\t\t\tns, err := cfg.CurrentNamespaceName()\n\t\t\tif ns != \"\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t\tassert.Equal(t, u.namespace, ns)\n\t\t})\n\t}\n}\n\nfunc TestConfigGetContext(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcluster string\n\t\terr     error\n\t}{\n\t\t\"default\": {\n\t\t\tcluster: \"blee\",\n\t\t},\n\t\t\"custom\": {\n\t\t\tcluster: \"bozo\",\n\t\t\terr:     errors.New(`getcontext - invalid context specified: \"bozo\"`),\n\t\t},\n\t}\n\n\tflags := &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}\n\tcfg := client.NewConfig(flags)\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tctx, err := cfg.GetContext(u.cluster)\n\t\t\tif err != nil {\n\t\t\t\tassert.Equal(t, u.err, err)\n\t\t\t} else {\n\t\t\t\tassert.NotNil(t, ctx)\n\t\t\t\tassert.Equal(t, u.cluster, ctx.Cluster)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestConfigSwitchContext(t *testing.T) {\n\tcluster := \"duh\"\n\tflags := genericclioptions.ConfigFlags{\n\t\tKubeConfig: &kubeConfig,\n\t\tContext:    &cluster,\n\t}\n\n\tcfg := client.NewConfig(&flags)\n\terr := cfg.SwitchContext(\"blee\")\n\trequire.NoError(t, err)\n\tctx, err := cfg.CurrentContextName()\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"blee\", ctx)\n}\n\nfunc TestConfigAccess(t *testing.T) {\n\tcontext := \"duh\"\n\tflags := genericclioptions.ConfigFlags{\n\t\tKubeConfig: &kubeConfig,\n\t\tContext:    &context,\n\t}\n\n\tcfg := client.NewConfig(&flags)\n\tacc, err := cfg.ConfigAccess()\n\trequire.NoError(t, err)\n\tassert.NotEmpty(t, acc.GetDefaultFilename())\n}\n\nfunc TestConfigContextNames(t *testing.T) {\n\tcluster := \"duh\"\n\tflags := genericclioptions.ConfigFlags{\n\t\tKubeConfig: &kubeConfig,\n\t\tContext:    &cluster,\n\t}\n\n\tcfg := client.NewConfig(&flags)\n\tcc, err := cfg.ContextNames()\n\trequire.NoError(t, err)\n\tassert.Len(t, cc, 3)\n}\n\nfunc TestConfigContexts(t *testing.T) {\n\tcontext := \"duh\"\n\tflags := genericclioptions.ConfigFlags{\n\t\tKubeConfig: &kubeConfig,\n\t\tContext:    &context,\n\t}\n\n\tcfg := client.NewConfig(&flags)\n\tcc, err := cfg.Contexts()\n\trequire.NoError(t, err)\n\tassert.Len(t, cc, 3)\n}\n\nfunc TestConfigDelContext(t *testing.T) {\n\trequire.NoError(t, cp(\"./testdata/config.2\", \"./testdata/config.1\"))\n\n\tcontext, kubeCfg := \"duh\", \"./testdata/config.1\"\n\tflags := genericclioptions.ConfigFlags{\n\t\tKubeConfig: &kubeCfg,\n\t\tContext:    &context,\n\t}\n\n\tcfg := client.NewConfig(&flags)\n\terr := cfg.DelContext(\"fred\")\n\trequire.NoError(t, err)\n\n\tcc, err := cfg.ContextNames()\n\trequire.NoError(t, err)\n\tassert.Len(t, cc, 1)\n\t_, ok := cc[\"blee\"]\n\tassert.True(t, ok)\n}\n\nfunc TestConfigRestConfig(t *testing.T) {\n\tflags := genericclioptions.ConfigFlags{\n\t\tKubeConfig: &kubeConfig,\n\t}\n\n\tcfg := client.NewConfig(&flags)\n\trc, err := cfg.RESTConfig()\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"https://localhost:3002\", rc.Host)\n}\n\nfunc TestConfigBadConfig(t *testing.T) {\n\tkubeConfig := \"./testdata/bork_config\"\n\tflags := genericclioptions.ConfigFlags{\n\t\tKubeConfig: &kubeConfig,\n\t}\n\n\tcfg := client.NewConfig(&flags)\n\t_, err := cfg.RESTConfig()\n\tassert.Error(t, err)\n}\n\n// Helpers...\n\nfunc cp(src, dst string) error {\n\tdata, err := os.ReadFile(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn os.WriteFile(dst, data, 0600)\n}\n"
  },
  {
    "path": "internal/client/errors.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage client\n\nimport metricsapi \"k8s.io/metrics/pkg/apis/metrics\"\n\n// Error represents an error.\ntype Error string\n\n// Error returns the error text.\nfunc (e Error) Error() string {\n\treturn string(e)\n}\n\nconst (\n\tnoMetricServerErr     = Error(\"No metrics-server detected\")\n\tmetricsUnsupportedErr = Error(\"No metrics api group \" + metricsapi.GroupName + \" found on cluster\")\n)\n"
  },
  {
    "path": "internal/client/gvr.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage client\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"path\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/fvbommel/sortorder\"\n\t\"gopkg.in/yaml.v3\"\n\tapiext \"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n)\n\nvar NoGVR = new(GVR)\n\n// GVR represents a kubernetes resource schema as a string.\n// Format is group/version/resources:subresource.\ntype GVR struct {\n\traw, g, v, r, sr string\n}\n\ntype gvrCache struct {\n\tdata map[string]*GVR\n\tsync.RWMutex\n}\n\nfunc (c *gvrCache) add(gvr *GVR) {\n\tif c.get(gvr.String()) == nil {\n\t\tc.Lock()\n\t\tc.data[gvr.String()] = gvr\n\t\tc.Unlock()\n\t}\n}\n\nfunc (c *gvrCache) get(gvrs string) *GVR {\n\tc.RLock()\n\tdefer c.RUnlock()\n\n\tif gvr, ok := c.data[gvrs]; ok {\n\t\treturn gvr\n\t}\n\n\treturn nil\n}\n\nvar gvrsCache = gvrCache{\n\tdata: make(map[string]*GVR),\n}\n\n// NewGVR builds a new gvr from a group, version, resource.\nfunc NewGVR(s string) *GVR {\n\traw := s\n\ttokens := strings.Split(s, \":\")\n\tvar g, v, r, sr string\n\tif len(tokens) == 2 {\n\t\traw, sr = tokens[0], tokens[1]\n\t}\n\ttokens = strings.Split(raw, \"/\")\n\tswitch len(tokens) {\n\tcase 3:\n\t\tg, v, r = tokens[0], tokens[1], tokens[2]\n\tcase 2:\n\t\tv, r = tokens[0], tokens[1]\n\tcase 1:\n\t\tr = tokens[0]\n\tdefault:\n\t\tslog.Error(\"GVR init failed!\", slogs.Error, fmt.Errorf(\"can't parse GVR %q\", s))\n\t}\n\n\tgvr := GVR{raw: s, g: g, v: v, r: r, sr: sr}\n\tif cgvr := gvrsCache.get(gvr.String()); cgvr != nil {\n\t\treturn cgvr\n\t}\n\tgvrsCache.add(&gvr)\n\n\treturn &gvr\n}\n\nfunc (g *GVR) IsAlias() bool {\n\treturn !g.IsK8sRes()\n}\n\nfunc (g *GVR) IsK8sRes() bool {\n\treturn g != nil && ((!strings.Contains(g.raw, \" \") && strings.Contains(g.raw, \"/\") && !strings.Contains(g.raw, \" /\")) || reservedGVRs.Has(g))\n}\n\n// WithSubResource builds a new gvr with a sub resource.\nfunc (g *GVR) WithSubResource(sub string) *GVR {\n\treturn NewGVR(g.String() + \":\" + sub)\n}\n\n// NewGVRFromMeta builds a gvr from resource metadata.\nfunc NewGVRFromMeta(a *metav1.APIResource) *GVR {\n\treturn NewGVR(path.Join(a.Group, a.Version, a.Name))\n}\n\n// NewGVRFromCRD builds a gvr from a custom resource definition.\nfunc NewGVRFromCRD(crd *apiext.CustomResourceDefinition) map[*GVR]*apiext.CustomResourceDefinitionVersion {\n\tmm := make(map[*GVR]*apiext.CustomResourceDefinitionVersion, len(crd.Spec.Versions))\n\tfor _, v := range crd.Spec.Versions {\n\t\tif v.Served && !v.Deprecated {\n\t\t\tgvr := NewGVRFromMeta(&metav1.APIResource{\n\t\t\t\tKind:    crd.Spec.Names.Kind,\n\t\t\t\tGroup:   crd.Spec.Group,\n\t\t\t\tName:    crd.Spec.Names.Plural,\n\t\t\t\tVersion: v.Name,\n\t\t\t})\n\t\t\tmm[gvr] = &v\n\t\t}\n\t}\n\n\treturn mm\n}\n\n// FromGVAndR builds a gvr from a group/version and resource.\nfunc FromGVAndR(gv, r string) *GVR {\n\treturn NewGVR(path.Join(gv, r))\n}\n\n// FQN returns a fully qualified resource name.\nfunc (g *GVR) FQN(n string) string {\n\treturn path.Join(g.AsResourceName(), n)\n}\n\n// AsResourceName returns a resource . separated descriptor in the shape of kind.version.group.\nfunc (g *GVR) AsResourceName() string {\n\tif g.g == \"\" {\n\t\treturn g.r\n\t}\n\n\treturn g.r + \".\" + g.v + \".\" + g.g\n}\n\n// SubResource returns a sub resource if available.\nfunc (g *GVR) SubResource() string {\n\treturn g.sr\n}\n\n// String returns gvr as string.\nfunc (g *GVR) String() string {\n\treturn g.raw\n}\n\n// GV returns the group version scheme representation.\nfunc (g *GVR) GV() schema.GroupVersion {\n\treturn schema.GroupVersion{\n\t\tGroup:   g.g,\n\t\tVersion: g.v,\n\t}\n}\n\n// GVK returns a full schema representation.\nfunc (g *GVR) GVK() schema.GroupVersionKind {\n\treturn schema.GroupVersionKind{\n\t\tGroup:   g.G(),\n\t\tVersion: g.V(),\n\t\tKind:    g.R(),\n\t}\n}\n\n// GVR returns a full schema representation.\nfunc (g *GVR) GVR() schema.GroupVersionResource {\n\treturn schema.GroupVersionResource{\n\t\tGroup:    g.G(),\n\t\tVersion:  g.V(),\n\t\tResource: g.R(),\n\t}\n}\n\n// GVSub returns group vervion sub path.\nfunc (g *GVR) GVSub() string {\n\tif g.G() == \"\" {\n\t\treturn g.V()\n\t}\n\n\treturn g.G() + \"/\" + g.V()\n}\n\n// GR returns a full schema representation.\nfunc (g *GVR) GR() *schema.GroupResource {\n\treturn &schema.GroupResource{\n\t\tGroup:    g.G(),\n\t\tResource: g.R(),\n\t}\n}\n\n// V returns the resource version.\nfunc (g *GVR) V() string {\n\treturn g.v\n}\n\n// RG returns the resource and group.\nfunc (g *GVR) RG() (resource, group string) {\n\treturn g.r, g.g\n}\n\n// R returns the resource name.\nfunc (g *GVR) R() string {\n\treturn g.r\n}\n\n// G returns the resource group name.\nfunc (g *GVR) G() string {\n\treturn g.g\n}\n\n// IsDecodable checks if the k8s resource has a decodable view\nfunc (g *GVR) IsDecodable() bool {\n\treturn g == SecGVR\n}\n\nvar _ = yaml.Marshaler((*GVR)(nil))\nvar _ = yaml.Unmarshaler((*GVR)(nil))\n\nfunc (g *GVR) MarshalYAML() (any, error) {\n\treturn g.String(), nil\n}\n\nfunc (g *GVR) UnmarshalYAML(n *yaml.Node) error {\n\t*g = *NewGVR(n.Value)\n\n\treturn nil\n}\n\n// GVRs represents a collection of gvr.\ntype GVRs []*GVR\n\n// Len returns the list size.\nfunc (g GVRs) Len() int {\n\treturn len(g)\n}\n\n// Swap swaps list values.\nfunc (g GVRs) Swap(i, j int) {\n\tg[i], g[j] = g[j], g[i]\n}\n\n// Less returns true if i < j.\nfunc (g GVRs) Less(i, j int) bool {\n\tg1, g2 := g[i].G(), g[j].G()\n\n\treturn sortorder.NaturalLess(g1, g2)\n}\n\n// Helper...\n\n// Can determines the available actions for a given resource.\nfunc Can(verbs []string, v string) bool {\n\tif verbs == nil {\n\t\treturn true\n\t}\n\tif len(verbs) == 0 {\n\t\treturn false\n\t}\n\tfor _, verb := range verbs {\n\t\tcandidates, err := mapVerb(v)\n\t\tif err != nil {\n\t\t\tslog.Error(\"Access verb mapping failed\", slogs.Error, err)\n\t\t\treturn false\n\t\t}\n\t\tfor _, c := range candidates {\n\t\t\tif verb == c {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc mapVerb(v string) ([]string, error) {\n\tswitch v {\n\tcase \"describe\":\n\t\treturn []string{\"get\"}, nil\n\tcase \"view\":\n\t\treturn []string{\"get\", \"list\"}, nil\n\tcase \"delete\":\n\t\treturn []string{\"delete\"}, nil\n\tcase \"edit\":\n\t\treturn []string{\"patch\", \"update\"}, nil\n\tdefault:\n\t\treturn []string{}, fmt.Errorf(\"no standard verb for %q\", v)\n\t}\n}\n"
  },
  {
    "path": "internal/client/gvr_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage client_test\n\nimport (\n\t\"path\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n)\n\nfunc TestGVRSort(t *testing.T) {\n\tgg := client.GVRs{\n\t\tclient.PodGVR,\n\t\tclient.SvcGVR,\n\t\tclient.DpGVR,\n\t}\n\tsort.Sort(gg)\n\tassert.Equal(t, client.GVRs{\n\t\tclient.PodGVR,\n\t\tclient.SvcGVR,\n\t\tclient.DpGVR,\n\t}, gg)\n}\n\nfunc TestGVRCan(t *testing.T) {\n\tuu := map[string]struct {\n\t\tvv []string\n\t\tv  string\n\t\te  bool\n\t}{\n\t\t\"describe\":  {[]string{\"get\"}, \"describe\", true},\n\t\t\"view\":      {[]string{\"get\", \"list\", \"watch\"}, \"view\", true},\n\t\t\"delete\":    {[]string{\"delete\", \"list\", \"watch\"}, \"delete\", true},\n\t\t\"no_delete\": {[]string{\"get\", \"list\", \"watch\"}, \"delete\", false},\n\t\t\"edit\":      {[]string{\"path\", \"update\", \"watch\"}, \"edit\", true},\n\t\t\"no_edit\":   {[]string{\"get\", \"list\", \"watch\"}, \"edit\", false},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, client.Can(u.vv, u.v))\n\t\t})\n\t}\n}\n\nfunc TestGVR(t *testing.T) {\n\tuu := map[string]struct {\n\t\tgvr string\n\t\te   schema.GroupVersionResource\n\t}{\n\t\t\"full\": {client.DpGVR.String(), schema.GroupVersionResource{Group: \"apps\", Version: \"v1\", Resource: \"deployments\"}},\n\t\t\"core\": {client.PodGVR.String(), schema.GroupVersionResource{Version: \"v1\", Resource: \"pods\"}},\n\t\t\"bork\": {client.UsrGVR.String(), schema.GroupVersionResource{Resource: \"users\"}},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, client.NewGVR(u.gvr).GVR())\n\t\t})\n\t}\n}\n\nfunc TestAsGV(t *testing.T) {\n\tuu := map[string]struct {\n\t\tgvr string\n\t\te   schema.GroupVersion\n\t}{\n\t\t\"full\": {client.DpGVR.String(), schema.GroupVersion{Group: \"apps\", Version: \"v1\"}},\n\t\t\"core\": {client.PodGVR.String(), schema.GroupVersion{Version: \"v1\"}},\n\t\t\"bork\": {client.UsrGVR.String(), schema.GroupVersion{}},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, client.NewGVR(u.gvr).GV())\n\t\t})\n\t}\n}\n\nfunc TestNewGVR(t *testing.T) {\n\tuu := map[string]struct {\n\t\tg, v, r string\n\t\te       string\n\t}{\n\t\t\"full\": {\"apps\", \"v1\", \"deployments\", client.DpGVR.String()},\n\t\t\"core\": {\"\", \"v1\", \"pods\", client.PodGVR.String()},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, client.NewGVR(path.Join(u.g, u.v, u.r)).String())\n\t\t})\n\t}\n}\n\nfunc TestGVRAsResourceName(t *testing.T) {\n\tuu := map[string]struct {\n\t\tgvr string\n\t\te   string\n\t}{\n\t\t\"full\":  {client.DpGVR.String(), \"deployments.v1.apps\"},\n\t\t\"core\":  {client.PodGVR.String(), \"pods\"},\n\t\t\"k9s\":   {client.UsrGVR.String(), \"users\"},\n\t\t\"empty\": {\"\", \"\"},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, client.NewGVR(u.gvr).AsResourceName())\n\t\t})\n\t}\n}\n\nfunc TestToR(t *testing.T) {\n\tuu := map[string]struct {\n\t\tgvr string\n\t\te   string\n\t}{\n\t\t\"full\":  {client.DpGVR.String(), \"deployments\"},\n\t\t\"core\":  {client.PodGVR.String(), \"pods\"},\n\t\t\"k9s\":   {client.UsrGVR.String(), \"users\"},\n\t\t\"empty\": {\"\", \"\"},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, client.NewGVR(u.gvr).R())\n\t\t})\n\t}\n}\n\nfunc TestToG(t *testing.T) {\n\tuu := map[string]struct {\n\t\tgvr string\n\t\te   string\n\t}{\n\t\t\"full\":  {client.DpGVR.String(), \"apps\"},\n\t\t\"core\":  {client.PodGVR.String(), \"\"},\n\t\t\"k9s\":   {client.UsrGVR.String(), \"\"},\n\t\t\"empty\": {\"\", \"\"},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, client.NewGVR(u.gvr).G())\n\t\t})\n\t}\n}\n\nfunc TestToV(t *testing.T) {\n\tuu := map[string]struct {\n\t\tgvr string\n\t\te   string\n\t}{\n\t\t\"full\":  {client.DpGVR.String(), \"v1\"},\n\t\t\"core\":  {\"v1beta1/pods\", \"v1beta1\"},\n\t\t\"k9s\":   {client.UsrGVR.String(), \"\"},\n\t\t\"empty\": {\"\", \"\"},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, client.NewGVR(u.gvr).V())\n\t\t})\n\t}\n}\n\nfunc TestToString(t *testing.T) {\n\tuu := map[string]struct {\n\t\tgvr string\n\t}{\n\t\t\"full\":  {client.DpGVR.String()},\n\t\t\"core\":  {\"v1beta1/pods\"},\n\t\t\"k9s\":   {client.UsrGVR.String()},\n\t\t\"empty\": {\"\"},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.gvr, client.NewGVR(u.gvr).String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/client/gvrs.go",
    "content": "package client\n\nimport \"k8s.io/apimachinery/pkg/util/sets\"\n\nvar (\n\t// Apps...\n\tDpGVR  = NewGVR(\"apps/v1/deployments\")\n\tStsGVR = NewGVR(\"apps/v1/statefulsets\")\n\tDsGVR  = NewGVR(\"apps/v1/daemonsets\")\n\tRsGVR  = NewGVR(\"apps/v1/replicasets\")\n\tRcGVR  = NewGVR(\"apps/v1/replicationcontrollers\")\n\n\t// Core...\n\tSaGVR   = NewGVR(\"v1/serviceaccounts\")\n\tPvcGVR  = NewGVR(\"v1/persistentvolumeclaims\")\n\tPvGVR   = NewGVR(\"v1/persistentvolumes\")\n\tCmGVR   = NewGVR(\"v1/configmaps\")\n\tSecGVR  = NewGVR(\"v1/secrets\")\n\tEvGVR   = NewGVR(\"events.k8s.io/v1/events\")\n\tEpGVR   = NewGVR(\"v1/endpoints\")\n\tPodGVR  = NewGVR(\"v1/pods\")\n\tNsGVR   = NewGVR(\"v1/namespaces\")\n\tNodeGVR = NewGVR(\"v1/nodes\")\n\tSvcGVR  = NewGVR(\"v1/services\")\n\n\t// Discovery...\n\tEpsGVR = NewGVR(\"discovery.k8s.io/v1/endpointslices\")\n\n\t// Autoscaling...\n\tHpaGVR = NewGVR(\"autoscaling/v1/horizontalpodautoscalers\")\n\n\t// Batch...\n\tCjGVR  = NewGVR(\"batch/v1/cronjobs\")\n\tJobGVR = NewGVR(\"batch/v1/jobs\")\n\n\t// Misc...\n\tCrdGVR = NewGVR(\"apiextensions.k8s.io/v1/customresourcedefinitions\")\n\tPcGVR  = NewGVR(\"scheduling.k8s.io/v1/priorityclasses\")\n\tNpGVR  = NewGVR(\"networking.k8s.io/v1/networkpolicies\")\n\tScGVR  = NewGVR(\"storage.k8s.io/v1/storageclasses\")\n\n\t// Policy...\n\tPdbGVR = NewGVR(\"policy/v1/poddisruptionbudgets\")\n\tPspGVR = NewGVR(\"policy/v1beta1/podsecuritypolicies\")\n\n\tIngGVR = NewGVR(\"networking.k8s.io/v1/ingresses\")\n\n\t// Metrics...\n\tNmxGVR = NewGVR(\"metrics.k8s.io/v1beta1/nodes\")\n\tPmxGVR = NewGVR(\"metrics.k8s.io/v1beta1/pods\")\n\n\t// K9s...\n\tCpuGVR = NewGVR(\"cpu\")\n\tMemGVR = NewGVR(\"memory\")\n\tWkGVR  = NewGVR(\"workloads\")\n\tCoGVR  = NewGVR(\"containers\")\n\tCtGVR  = NewGVR(\"contexts\")\n\tRefGVR = NewGVR(\"references\")\n\tPuGVR  = NewGVR(\"pulses\")\n\tScnGVR = NewGVR(\"scans\")\n\tDirGVR = NewGVR(\"dirs\")\n\tPfGVR  = NewGVR(\"portforwards\")\n\tSdGVR  = NewGVR(\"screendumps\")\n\tBeGVR  = NewGVR(\"benchmarks\")\n\tAliGVR = NewGVR(\"aliases\")\n\tXGVR   = NewGVR(\"xrays\")\n\tHlpGVR = NewGVR(\"help\")\n\tQGVR   = NewGVR(\"quit\")\n\n\t// Helm...\n\tHmGVR  = NewGVR(\"helm\")\n\tHmhGVR = NewGVR(\"helm-history\")\n\n\t// RBAC...\n\tRbacGVR = NewGVR(\"rbac\")\n\tPolGVR  = NewGVR(\"policy\")\n\tUsrGVR  = NewGVR(\"users\")\n\tGrpGVR  = NewGVR(\"groups\")\n\tCrGVR   = NewGVR(\"rbac.authorization.k8s.io/v1/clusterroles\")\n\tCrbGVR  = NewGVR(\"rbac.authorization.k8s.io/v1/clusterrolebindings\")\n\tRoGVR   = NewGVR(\"rbac.authorization.k8s.io/v1/roles\")\n\tRobGVR  = NewGVR(\"rbac.authorization.k8s.io/v1/rolebindings\")\n)\n\nvar reservedGVRs = sets.New(\n\tCpuGVR,\n\tMemGVR,\n\tWkGVR,\n\tCoGVR,\n\tCtGVR,\n\tRefGVR,\n\tPuGVR,\n\tScnGVR,\n\tDirGVR,\n\tPfGVR,\n\tSdGVR,\n\tBeGVR,\n\tAliGVR,\n\tXGVR,\n\tHlpGVR,\n\tQGVR,\n\tHmGVR,\n\tHmhGVR,\n\tRbacGVR,\n\tPolGVR,\n\tUsrGVR,\n\tGrpGVR,\n)\n"
  },
  {
    "path": "internal/client/helper_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage client_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/stretchr/testify/assert\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nfunc TestMetaFQN(t *testing.T) {\n\tuu := map[string]struct {\n\t\tmeta metav1.ObjectMeta\n\t\te    string\n\t}{\n\t\t\"empty\": {\n\t\t\te: \"-/\",\n\t\t},\n\t\t\"full\": {\n\t\t\tmeta: metav1.ObjectMeta{Name: \"blee\", Namespace: \"ns1\"},\n\t\t\te:    \"ns1/blee\",\n\t\t},\n\t\t\"no-ns\": {\n\t\t\tmeta: metav1.ObjectMeta{Name: \"blee\"},\n\t\t\te:    \"-/blee\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, client.MetaFQN(&u.meta))\n\t\t})\n\t}\n}\n\nfunc TestCoFQN(t *testing.T) {\n\tuu := map[string]struct {\n\t\tmeta metav1.ObjectMeta\n\t\tco   string\n\t\te    string\n\t}{\n\t\t\"empty\": {\n\t\t\te: \"-/:\",\n\t\t},\n\t\t\"full\": {\n\t\t\tmeta: metav1.ObjectMeta{Name: \"blee\", Namespace: \"ns1\"},\n\t\t\tco:   \"fred\",\n\t\t\te:    \"ns1/blee:fred\",\n\t\t},\n\t\t\"no-co\": {\n\t\t\tmeta: metav1.ObjectMeta{Name: \"blee\", Namespace: \"ns1\"},\n\t\t\te:    \"ns1/blee:\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, client.CoFQN(&u.meta, u.co))\n\t\t})\n\t}\n}\n\nfunc TestIsClusterScoped(t *testing.T) {\n\tuu := map[string]struct {\n\t\tns string\n\t\te  bool\n\t}{\n\t\t\"empty\": {},\n\t\t\"all\": {\n\t\t\tns: client.NamespaceAll,\n\t\t},\n\t\t\"none\": {\n\t\t\tns: client.BlankNamespace,\n\t\t},\n\t\t\"custom\": {\n\t\t\tns: \"fred\",\n\t\t},\n\t\t\"scoped\": {\n\t\t\tns: \"-\",\n\t\t\te:  true,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, client.IsClusterScoped(u.ns))\n\t\t})\n\t}\n}\n\nfunc TestIsNamespaced(t *testing.T) {\n\tuu := map[string]struct {\n\t\tns string\n\t\te  bool\n\t}{\n\t\t\"empty\": {},\n\t\t\"all\": {\n\t\t\tns: client.NamespaceAll,\n\t\t},\n\t\t\"cluster\": {\n\t\t\tns: client.ClusterScope,\n\t\t},\n\t\t\"none\": {\n\t\t\tns: client.BlankNamespace,\n\t\t},\n\t\t\"custom\": {\n\t\t\tns: \"fred\",\n\t\t\te:  true,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, client.IsNamespaced(u.ns))\n\t\t})\n\t}\n}\n\nfunc TestIsAllNamespaces(t *testing.T) {\n\tuu := map[string]struct {\n\t\tns string\n\t\te  bool\n\t}{\n\t\t\"empty\": {\n\t\t\te: true,\n\t\t},\n\t\t\"all\": {\n\t\t\tns: client.NamespaceAll,\n\t\t\te:  true,\n\t\t},\n\t\t\"none\": {\n\t\t\tns: client.BlankNamespace,\n\t\t\te:  true,\n\t\t},\n\t\t\"custom\": {\n\t\t\tns: \"fred\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, client.IsAllNamespaces(u.ns))\n\t\t})\n\t}\n}\n\nfunc TestIsAllNamespace(t *testing.T) {\n\tuu := map[string]struct {\n\t\tns string\n\t\te  bool\n\t}{\n\t\t\"empty\": {},\n\t\t\"all\": {\n\t\t\tns: client.NamespaceAll,\n\t\t\te:  true,\n\t\t},\n\t\t\"custom\": {\n\t\t\tns: \"fred\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, client.IsAllNamespace(u.ns))\n\t\t})\n\t}\n}\n\nfunc TestCleanseNamespace(t *testing.T) {\n\tuu := map[string]struct {\n\t\tns, e string\n\t}{\n\t\t\"empty\": {},\n\t\t\"all\": {\n\t\t\tns: client.NamespaceAll,\n\t\t\te:  client.BlankNamespace,\n\t\t},\n\t\t\"custom\": {\n\t\t\tns: \"fred\",\n\t\t\te:  \"fred\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, client.CleanseNamespace(u.ns))\n\t\t})\n\t}\n}\n\nfunc TestNamespaced(t *testing.T) {\n\tuu := []struct {\n\t\tp, ns, n string\n\t}{\n\t\t{\"fred/blee\", \"fred\", \"blee\"},\n\t\t{\"blee\", \"\", \"blee\"},\n\t}\n\n\tfor _, u := range uu {\n\t\tns, n := client.Namespaced(u.p)\n\t\tassert.Equal(t, u.ns, ns)\n\t\tassert.Equal(t, u.n, n)\n\t}\n}\n\nfunc TestFQN(t *testing.T) {\n\tuu := []struct {\n\t\tns, n string\n\t\te     string\n\t}{\n\t\t{\"fred\", \"blee\", \"fred/blee\"},\n\t\t{\"\", \"blee\", \"blee\"},\n\t}\n\n\tfor _, u := range uu {\n\t\tassert.Equal(t, u.e, client.FQN(u.ns, u.n))\n\t}\n}\n"
  },
  {
    "path": "internal/client/helpers.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage client\n\nimport (\n\t\"log/slog\"\n\t\"os\"\n\t\"os/user\"\n\t\"path\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/slogs\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nvar toFileName = regexp.MustCompile(`[^(\\w/.)]`)\n\n// IsClusterWide returns true if ns designates cluster scope, false otherwise.\nfunc IsClusterWide(ns string) bool {\n\treturn ns == NamespaceAll || ns == BlankNamespace || ns == ClusterScope\n}\n\nfunc PrintNamespace(ns string) string {\n\tif IsAllNamespaces(ns) {\n\t\treturn \"all\"\n\t}\n\n\treturn ns\n}\n\n// CleanseNamespace ensures all ns maps to blank.\nfunc CleanseNamespace(ns string) string {\n\tif IsAllNamespace(ns) {\n\t\treturn BlankNamespace\n\t}\n\n\treturn ns\n}\n\n// IsAllNamespace returns true if ns == all.\nfunc IsAllNamespace(ns string) bool {\n\treturn ns == NamespaceAll\n}\n\n// IsAllNamespaces returns true if all namespaces, false otherwise.\nfunc IsAllNamespaces(ns string) bool {\n\treturn ns == NamespaceAll || ns == BlankNamespace\n}\n\n// IsNamespaced returns true if a specific ns is given.\nfunc IsNamespaced(ns string) bool {\n\treturn !IsAllNamespaces(ns) && !IsClusterScoped(ns)\n}\n\n// IsClusterScoped returns true if resource is not namespaced.\nfunc IsClusterScoped(ns string) bool {\n\treturn ns == ClusterScope\n}\n\n// Namespaced converts a resource path to namespace and resource name.\nfunc Namespaced(p string) (ns, name string) {\n\tns, name = path.Split(p)\n\n\treturn strings.Trim(ns, \"/\"), name\n}\n\n// CoFQN returns a fully qualified container name.\nfunc CoFQN(m *metav1.ObjectMeta, co string) string {\n\treturn MetaFQN(m) + \":\" + co\n}\n\n// FQN returns a fully qualified resource name.\nfunc FQN(ns, n string) string {\n\tif ns == \"\" {\n\t\treturn n\n\t}\n\treturn ns + \"/\" + n\n}\n\n// MetaFQN returns a fully qualified resource name.\nfunc MetaFQN(m *metav1.ObjectMeta) string {\n\tif m.Namespace == \"\" {\n\t\treturn FQN(ClusterScope, m.Name)\n\t}\n\n\treturn FQN(m.Namespace, m.Name)\n}\n\nfunc mustHomeDir() string {\n\tusr, err := user.Current()\n\tif err != nil {\n\t\tslog.Error(\"Die getting user home directory\", slogs.Error, err)\n\t\tos.Exit(1)\n\t}\n\treturn usr.HomeDir\n}\n\nfunc toHostDir(host string) string {\n\th := strings.Replace(\n\t\tstrings.Replace(host, \"https://\", \"\", 1),\n\t\t\"http://\", \"\", 1,\n\t)\n\treturn toFileName.ReplaceAllString(h, \"_\")\n}\n"
  },
  {
    "path": "internal/client/metrics.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage client\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"math\"\n\t\"strconv\"\n\t\"time\"\n\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/util/cache\"\n\tmv1beta1 \"k8s.io/metrics/pkg/apis/metrics/v1beta1\"\n)\n\nconst (\n\tmxCacheSize   = 100\n\tmxCacheExpiry = 1 * time.Minute\n)\n\n// MetricsDial tracks global metric server handle.\nvar MetricsDial *MetricsServer\n\n// DialMetrics dials the metrics server.\nfunc DialMetrics(c Connection) *MetricsServer {\n\tif MetricsDial == nil {\n\t\tMetricsDial = NewMetricsServer(c)\n\t}\n\n\treturn MetricsDial\n}\n\n// ResetMetrics resets the metric server handle.\nfunc ResetMetrics() {\n\tMetricsDial = nil\n}\n\n// MetricsServer serves cluster metrics for nodes and pods.\ntype MetricsServer struct {\n\tConnection\n\n\tcache *cache.LRUExpireCache\n}\n\n// NewMetricsServer return a metric server instance.\nfunc NewMetricsServer(c Connection) *MetricsServer {\n\treturn &MetricsServer{\n\t\tConnection: c,\n\t\tcache:      cache.NewLRUExpireCache(mxCacheSize),\n\t}\n}\n\n// ClusterLoad retrieves all cluster nodes metrics.\nfunc (*MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsList, mx *ClusterMetrics) error {\n\tif nos == nil || nmx == nil {\n\t\treturn fmt.Errorf(\"invalid node or node metrics lists\")\n\t}\n\tnodeMetrics := make(NodesMetrics, len(nos.Items))\n\tfor i := range nos.Items {\n\t\tnodeMetrics[nos.Items[i].Name] = NodeMetrics{\n\t\t\tAllocatableCPU: nos.Items[i].Status.Allocatable.Cpu().MilliValue(),\n\t\t\tAllocatableMEM: nos.Items[i].Status.Allocatable.Memory().Value(),\n\t\t}\n\t}\n\tfor i := range nmx.Items {\n\t\tif node, ok := nodeMetrics[nmx.Items[i].Name]; ok {\n\t\t\tnode.CurrentCPU = nmx.Items[i].Usage.Cpu().MilliValue()\n\t\t\tnode.CurrentMEM = nmx.Items[i].Usage.Memory().Value()\n\t\t\tnodeMetrics[nmx.Items[i].Name] = node\n\t\t}\n\t}\n\n\tvar ccpu, cmem, tcpu, tmem int64\n\tfor _, mx := range nodeMetrics {\n\t\tccpu += mx.CurrentCPU\n\t\tcmem += mx.CurrentMEM\n\t\ttcpu += mx.AllocatableCPU\n\t\ttmem += mx.AllocatableMEM\n\t}\n\tmx.PercCPU, mx.PercMEM = ToPercentage(ccpu, tcpu), ToPercentage(cmem, tmem)\n\n\treturn nil\n}\n\nfunc (m *MetricsServer) checkAccess(ns string, gvr *GVR, msg string) error {\n\tif !m.HasMetrics() {\n\t\treturn errors.New(\"no metrics-server detected on cluster\")\n\t}\n\n\tauth, err := m.CanI(ns, gvr, \"\", ListAccess)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !auth {\n\t\treturn errors.New(msg)\n\t}\n\treturn nil\n}\n\n// NodesMetrics retrieves metrics for a given set of nodes.\nfunc (*MetricsServer) NodesMetrics(nodes *v1.NodeList, metrics *mv1beta1.NodeMetricsList, mmx NodesMetrics) {\n\tif nodes == nil || metrics == nil {\n\t\treturn\n\t}\n\n\tfor i := range nodes.Items {\n\t\tmmx[nodes.Items[i].Name] = NodeMetrics{\n\t\t\tAllocatableCPU:       nodes.Items[i].Status.Allocatable.Cpu().MilliValue(),\n\t\t\tAllocatableMEM:       ToMB(nodes.Items[i].Status.Allocatable.Memory().Value()),\n\t\t\tAllocatableEphemeral: ToMB(nodes.Items[i].Status.Allocatable.StorageEphemeral().Value()),\n\t\t\tTotalCPU:             nodes.Items[i].Status.Capacity.Cpu().MilliValue(),\n\t\t\tTotalMEM:             ToMB(nodes.Items[i].Status.Capacity.Memory().Value()),\n\t\t\tTotalEphemeral:       ToMB(nodes.Items[i].Status.Capacity.StorageEphemeral().Value()),\n\t\t}\n\t}\n\tfor i := range metrics.Items {\n\t\tmx, ok := mmx[metrics.Items[i].Name]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tmx.CurrentCPU = metrics.Items[i].Usage.Cpu().MilliValue()\n\t\tmx.CurrentMEM = ToMB(metrics.Items[i].Usage.Memory().Value())\n\t\tmx.AvailableCPU = mx.AllocatableCPU - mx.CurrentCPU\n\t\tmx.AvailableMEM = mx.AllocatableMEM - mx.CurrentMEM\n\t\tmmx[metrics.Items[i].Name] = mx\n\t}\n}\n\n// FetchNodesMetricsMap fetch node metrics as a map.\nfunc (m *MetricsServer) FetchNodesMetricsMap(ctx context.Context) (NodesMetricsMap, error) {\n\tmm, err := m.FetchNodesMetrics(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thh := make(NodesMetricsMap, len(mm.Items))\n\tfor i := range mm.Items {\n\t\tmx := mm.Items[i]\n\t\thh[mx.Name] = &mx\n\t}\n\n\treturn hh, nil\n}\n\n// FetchNodesMetrics return all metrics for nodes.\nfunc (m *MetricsServer) FetchNodesMetrics(ctx context.Context) (*mv1beta1.NodeMetricsList, error) {\n\tconst msg = \"user is not authorized to list node metrics\"\n\n\tmx := new(mv1beta1.NodeMetricsList)\n\tif err := m.checkAccess(ClusterScope, NmxGVR, msg); err != nil {\n\t\treturn mx, err\n\t}\n\n\tconst key = \"nodes\"\n\tif entry, ok := m.cache.Get(key); ok && entry != nil {\n\t\tmxList, ok := entry.(*mv1beta1.NodeMetricsList)\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"expected nodemetricslist but got %T\", entry)\n\t\t}\n\t\treturn mxList, nil\n\t}\n\n\tclient, err := m.MXDial()\n\tif err != nil {\n\t\treturn mx, err\n\t}\n\tmxList, err := client.MetricsV1beta1().NodeMetricses().List(ctx, metav1.ListOptions{})\n\tif err != nil {\n\t\treturn mx, err\n\t}\n\tm.cache.Add(key, mxList, mxCacheExpiry)\n\n\treturn mxList, nil\n}\n\n// FetchNodeMetrics return all metrics for nodes.\nfunc (m *MetricsServer) FetchNodeMetrics(ctx context.Context, n string) (*mv1beta1.NodeMetrics, error) {\n\tconst msg = \"user is not authorized to list node metrics\"\n\n\tmx := new(mv1beta1.NodeMetrics)\n\tif err := m.checkAccess(ClusterScope, NmxGVR, msg); err != nil {\n\t\treturn mx, err\n\t}\n\n\tmmx, err := m.FetchNodesMetricsMap(ctx)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmx, ok := mmx[n]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unable to retrieve node metrics for %q\", n)\n\t}\n\treturn mx, nil\n}\n\n// FetchPodsMetricsMap fetch pods metrics as a map.\nfunc (m *MetricsServer) FetchPodsMetricsMap(ctx context.Context, ns string) (PodsMetricsMap, error) {\n\tmm, err := m.FetchPodsMetrics(ctx, ns)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thh := make(PodsMetricsMap, len(mm.Items))\n\tfor i := range mm.Items {\n\t\tmx := mm.Items[i]\n\t\thh[FQN(mx.Namespace, mx.Name)] = &mx\n\t}\n\n\treturn hh, nil\n}\n\n// FetchPodsMetrics return all metrics for pods in a given namespace.\nfunc (m *MetricsServer) FetchPodsMetrics(ctx context.Context, ns string) (*mv1beta1.PodMetricsList, error) {\n\tmx := new(mv1beta1.PodMetricsList)\n\tconst msg = \"user is not authorized to list pods metrics\"\n\n\tif ns == NamespaceAll {\n\t\tns = BlankNamespace\n\t}\n\tif err := m.checkAccess(ns, PmxGVR, msg); err != nil {\n\t\treturn mx, err\n\t}\n\n\tkey := FQN(ns, \"pods\")\n\tif entry, ok := m.cache.Get(key); ok {\n\t\tmxList, ok := entry.(*mv1beta1.PodMetricsList)\n\t\tif !ok {\n\t\t\treturn mx, fmt.Errorf(\"expected PodMetricsList but got %T\", entry)\n\t\t}\n\t\treturn mxList, nil\n\t}\n\n\tclient, err := m.MXDial()\n\tif err != nil {\n\t\treturn mx, err\n\t}\n\tmxList, err := client.MetricsV1beta1().PodMetricses(ns).List(ctx, metav1.ListOptions{})\n\tif err != nil {\n\t\treturn mx, err\n\t}\n\tm.cache.Add(key, mxList, mxCacheExpiry)\n\n\treturn mxList, err\n}\n\n// FetchContainersMetrics returns a pod's containers metrics.\nfunc (m *MetricsServer) FetchContainersMetrics(ctx context.Context, fqn string) (ContainersMetrics, error) {\n\tmm, err := m.FetchPodMetrics(ctx, fqn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcmx := make(ContainersMetrics, len(mm.Containers))\n\tfor i := range mm.Containers {\n\t\tc := mm.Containers[i]\n\t\tcmx[c.Name] = &c\n\t}\n\n\treturn cmx, nil\n}\n\n// FetchPodMetrics return all metrics for pods in a given namespace.\nfunc (m *MetricsServer) FetchPodMetrics(ctx context.Context, fqn string) (*mv1beta1.PodMetrics, error) {\n\tvar mx *mv1beta1.PodMetrics\n\tconst msg = \"user is not authorized to list pod metrics\"\n\n\tns, _ := Namespaced(fqn)\n\tif ns == NamespaceAll {\n\t\tns = BlankNamespace\n\t}\n\tif err := m.checkAccess(ns, PmxGVR, msg); err != nil {\n\t\treturn mx, err\n\t}\n\n\tmmx, err := m.FetchPodsMetricsMap(ctx, ns)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpmx, ok := mmx[fqn]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"unable to locate pod metrics for pod %q\", fqn)\n\t}\n\n\treturn pmx, nil\n}\n\n// PodsMetrics retrieves metrics for all pods in a given namespace.\nfunc (*MetricsServer) PodsMetrics(pods *mv1beta1.PodMetricsList, mmx PodsMetrics) {\n\tif pods == nil {\n\t\treturn\n\t}\n\n\t// Compute all pod's containers metrics.\n\tfor i := range pods.Items {\n\t\tvar mx PodMetrics\n\t\tfor _, c := range pods.Items[i].Containers {\n\t\t\tmx.CurrentCPU += c.Usage.Cpu().MilliValue()\n\t\t\tmx.CurrentMEM += ToMB(c.Usage.Memory().Value())\n\t\t}\n\t\tmmx[pods.Items[i].Namespace+\"/\"+pods.Items[i].Name] = mx\n\t}\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\n// MegaByte represents a megabyte.\nconst MegaByte = 1024 * 1024\n\n// ToMB converts bytes to megabytes.\nfunc ToMB(v int64) int64 {\n\treturn v / MegaByte\n}\n\n// ToPercentage computes percentage as string otherwise n/aa.\nfunc ToPercentage(v, dv int64) int {\n\tif dv == 0 {\n\t\treturn 0\n\t}\n\n\treturn int(math.Floor((float64(v) / float64(dv)) * 100))\n}\n\n// ToPercentageStr computes percentage, but if v2 is 0, it will return NAValue instead of 0.\nfunc ToPercentageStr(v, dv int64) string {\n\tif dv == 0 {\n\t\treturn NA\n\t}\n\n\treturn strconv.Itoa(ToPercentage(v, dv))\n}\n"
  },
  {
    "path": "internal/client/metrics_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage client_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/stretchr/testify/assert\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/api/resource\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\tv1beta1 \"k8s.io/metrics/pkg/apis/metrics/v1beta1\"\n)\n\nfunc TestToPercentage(t *testing.T) {\n\tuu := []struct {\n\t\tv1, v2 int64\n\t\te      int\n\t}{\n\t\t{0, 0, 0},\n\t\t{100, 200, 50},\n\t\t{200, 100, 200},\n\t\t{224, 4000, 5},\n\t}\n\n\tfor _, u := range uu {\n\t\tassert.Equal(t, u.e, client.ToPercentage(u.v1, u.v2))\n\t}\n}\n\nfunc TestToMB(t *testing.T) {\n\tuu := []struct {\n\t\tv int64\n\t\te int64\n\t}{\n\t\t{0, 0},\n\t\t{2 * client.MegaByte, 2},\n\t\t{10 * client.MegaByte, 10},\n\t}\n\n\tfor _, u := range uu {\n\t\tassert.Equal(t, u.e, client.ToMB(u.v))\n\t}\n}\n\nfunc TestPodsMetrics(t *testing.T) {\n\tuu := map[string]struct {\n\t\tmetrics *v1beta1.PodMetricsList\n\t\teSize   int\n\t\te       client.PodsMetrics\n\t}{\n\t\t\"dud\": {\n\t\t\teSize: 0,\n\t\t},\n\n\t\t\"ok\": {\n\t\t\tmetrics: &v1beta1.PodMetricsList{\n\t\t\t\tItems: []v1beta1.PodMetrics{\n\t\t\t\t\t*makeMxPod(\"p1\", \"1\", \"4Gi\"),\n\t\t\t\t\t*makeMxPod(\"p2\", \"50m\", \"1Mi\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\teSize: 2,\n\t\t\te: client.PodsMetrics{\n\t\t\t\t\"default/p1\": client.PodMetrics{\n\t\t\t\t\tCurrentCPU: 3000,\n\t\t\t\t\tCurrentMEM: 12288,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tm := client.NewMetricsServer(nil)\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tmmx := make(client.PodsMetrics)\n\t\t\tm.PodsMetrics(u.metrics, mmx)\n\n\t\t\tassert.Len(t, mmx, u.eSize)\n\t\t\tif u.eSize == 0 {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tmx, ok := mmx[\"default/p1\"]\n\t\t\tassert.True(t, ok)\n\t\t\tassert.Equal(t, u.e[\"default/p1\"], mx)\n\t\t})\n\t}\n}\n\nfunc BenchmarkPodsMetrics(b *testing.B) {\n\tm := client.NewMetricsServer(nil)\n\n\tmetrics := v1beta1.PodMetricsList{\n\t\tItems: []v1beta1.PodMetrics{\n\t\t\t*makeMxPod(\"p1\", \"1\", \"4Gi\"),\n\t\t\t*makeMxPod(\"p2\", \"50m\", \"1Mi\"),\n\t\t\t*makeMxPod(\"p3\", \"50m\", \"1Mi\"),\n\t\t},\n\t}\n\tmmx := make(client.PodsMetrics, 3)\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\tfor range b.N {\n\t\tm.PodsMetrics(&metrics, mmx)\n\t}\n}\n\nfunc TestNodesMetrics(t *testing.T) {\n\tuu := map[string]struct {\n\t\tnodes   *v1.NodeList\n\t\tmetrics *v1beta1.NodeMetricsList\n\t\teSize   int\n\t\te       client.NodesMetrics\n\t}{\n\t\t\"duds\": {\n\t\t\teSize: 0,\n\t\t},\n\t\t\"no_nodes\": {\n\t\t\tmetrics: &v1beta1.NodeMetricsList{\n\t\t\t\tItems: []v1beta1.NodeMetrics{\n\t\t\t\t\t*makeMxNode(\"n1\", \"10\", \"8Gi\"),\n\t\t\t\t\t*makeMxNode(\"n2\", \"50m\", \"1Mi\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\teSize: 0,\n\t\t},\n\t\t\"no_metrics\": {\n\t\t\tnodes: &v1.NodeList{\n\t\t\t\tItems: []v1.Node{\n\t\t\t\t\tmakeNode(\"n1\", \"32\", \"128Gi\", \"50m\", \"2Mi\"),\n\t\t\t\t\tmakeNode(\"n2\", \"8\", \"4Gi\", \"50m\", \"10Mi\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\teSize: 0,\n\t\t},\n\t\t\"ok\": {\n\t\t\tnodes: &v1.NodeList{\n\t\t\t\tItems: []v1.Node{\n\t\t\t\t\tmakeNode(\"n1\", \"32\", \"128Gi\", \"32\", \"128Gi\"),\n\t\t\t\t\tmakeNode(\"n2\", \"8\", \"4Gi\", \"8\", \"4Gi\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetrics: &v1beta1.NodeMetricsList{\n\t\t\t\tItems: []v1beta1.NodeMetrics{\n\t\t\t\t\t*makeMxNode(\"n1\", \"10\", \"8Gi\"),\n\t\t\t\t\t*makeMxNode(\"n2\", \"50m\", \"1Mi\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\teSize: 2,\n\t\t\te: client.NodesMetrics{\n\t\t\t\t\"n1\": client.NodeMetrics{\n\t\t\t\t\tTotalCPU:       32000,\n\t\t\t\t\tTotalMEM:       131072,\n\t\t\t\t\tAllocatableCPU: 32000,\n\t\t\t\t\tAllocatableMEM: 131072,\n\t\t\t\t\tAvailableCPU:   22000,\n\t\t\t\t\tAvailableMEM:   122880,\n\t\t\t\t\tCurrentMetrics: client.CurrentMetrics{\n\t\t\t\t\t\tCurrentCPU: 10000,\n\t\t\t\t\t\tCurrentMEM: 8192,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tm := client.NewMetricsServer(nil)\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tmmx := make(client.NodesMetrics)\n\t\t\tm.NodesMetrics(u.nodes, u.metrics, mmx)\n\n\t\t\tassert.Len(t, mmx, u.eSize)\n\t\t\tif u.eSize == 0 {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tmx, ok := mmx[\"n1\"]\n\t\t\tassert.True(t, ok)\n\t\t\tassert.Equal(t, u.e[\"n1\"], mx)\n\t\t})\n\t}\n}\n\nfunc BenchmarkNodesMetrics(b *testing.B) {\n\tnodes := v1.NodeList{\n\t\tItems: []v1.Node{\n\t\t\tmakeNode(\"n1\", \"100m\", \"4Mi\", \"100m\", \"2Mi\"),\n\t\t\tmakeNode(\"n2\", \"100m\", \"4Mi\", \"100m\", \"2Mi\"),\n\t\t},\n\t}\n\n\tmetrics := v1beta1.NodeMetricsList{\n\t\tItems: []v1beta1.NodeMetrics{\n\t\t\t*makeMxNode(\"n1\", \"50m\", \"1Mi\"),\n\t\t\t*makeMxNode(\"n2\", \"50m\", \"1Mi\"),\n\t\t},\n\t}\n\n\tm := client.NewMetricsServer(nil)\n\tmmx := make(client.NodesMetrics)\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\tfor range b.N {\n\t\tm.NodesMetrics(&nodes, &metrics, mmx)\n\t}\n}\n\nfunc TestClusterLoad(t *testing.T) {\n\tuu := map[string]struct {\n\t\tnodes   *v1.NodeList\n\t\tmetrics *v1beta1.NodeMetricsList\n\t\teSize   int\n\t\te       client.ClusterMetrics\n\t}{\n\t\t\"duds\": {\n\t\t\teSize: 0,\n\t\t},\n\t\t\"no_nodes\": {\n\t\t\tmetrics: &v1beta1.NodeMetricsList{\n\t\t\t\tItems: []v1beta1.NodeMetrics{\n\t\t\t\t\t*makeMxNode(\"n1\", \"10\", \"8Gi\"),\n\t\t\t\t\t*makeMxNode(\"n2\", \"50m\", \"1Mi\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\teSize: 0,\n\t\t},\n\t\t\"no_metrics\": {\n\t\t\tnodes: &v1.NodeList{\n\t\t\t\tItems: []v1.Node{\n\t\t\t\t\tmakeNode(\"n1\", \"32\", \"128Gi\", \"50m\", \"2Mi\"),\n\t\t\t\t\tmakeNode(\"n2\", \"8\", \"4Gi\", \"50m\", \"10Mi\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\teSize: 0,\n\t\t},\n\t\t\"ok\": {\n\t\t\tnodes: &v1.NodeList{\n\t\t\t\tItems: []v1.Node{\n\t\t\t\t\tmakeNode(\"n1\", \"100m\", \"4Mi\", \"50m\", \"2Mi\"),\n\t\t\t\t\tmakeNode(\"n2\", \"100m\", \"4Mi\", \"50m\", \"2Mi\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tmetrics: &v1beta1.NodeMetricsList{\n\t\t\t\tItems: []v1beta1.NodeMetrics{\n\t\t\t\t\t*makeMxNode(\"n1\", \"50m\", \"1Mi\"),\n\t\t\t\t\t*makeMxNode(\"n2\", \"50m\", \"1Mi\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\teSize: 2,\n\t\t\te: client.ClusterMetrics{\n\t\t\t\tPercCPU: 100.0,\n\t\t\t\tPercMEM: 50.0,\n\t\t\t},\n\t\t},\n\t}\n\n\tm := client.NewMetricsServer(nil)\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tvar cmx client.ClusterMetrics\n\t\t\t_ = m.ClusterLoad(u.nodes, u.metrics, &cmx)\n\t\t\tassert.Equal(t, u.e, cmx)\n\t\t})\n\t}\n}\n\nfunc BenchmarkClusterLoad(b *testing.B) {\n\tnodes := v1.NodeList{\n\t\tItems: []v1.Node{\n\t\t\tmakeNode(\"n1\", \"100m\", \"4Mi\", \"50m\", \"2Mi\"),\n\t\t\tmakeNode(\"n2\", \"100m\", \"4Mi\", \"50m\", \"2Mi\"),\n\t\t},\n\t}\n\n\tmetrics := v1beta1.NodeMetricsList{\n\t\tItems: []v1beta1.NodeMetrics{\n\t\t\t*makeMxNode(\"n1\", \"50m\", \"1Mi\"),\n\t\t\t*makeMxNode(\"n2\", \"50m\", \"1Mi\"),\n\t\t},\n\t}\n\n\tm := client.NewMetricsServer(nil)\n\tvar mx client.ClusterMetrics\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\tfor range b.N {\n\t\t_ = m.ClusterLoad(&nodes, &metrics, &mx)\n\t}\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc makeMxPod(name, cpu, mem string) *v1beta1.PodMetrics {\n\treturn &v1beta1.PodMetrics{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:      name,\n\t\t\tNamespace: \"default\",\n\t\t},\n\t\tContainers: []v1beta1.ContainerMetrics{\n\t\t\t{Usage: makeRes(cpu, mem)},\n\t\t\t{Usage: makeRes(cpu, mem)},\n\t\t\t{Usage: makeRes(cpu, mem)},\n\t\t},\n\t}\n}\n\nfunc makeNode(name, tcpu, tmem, acpu, amem string) v1.Node {\n\treturn v1.Node{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName: name,\n\t\t},\n\t\tStatus: v1.NodeStatus{\n\t\t\tCapacity:    makeRes(tcpu, tmem),\n\t\t\tAllocatable: makeRes(acpu, amem),\n\t\t},\n\t}\n}\n\nfunc makeMxNode(name, cpu, mem string) *v1beta1.NodeMetrics {\n\treturn &v1beta1.NodeMetrics{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName: name,\n\t\t},\n\t\tUsage: makeRes(cpu, mem),\n\t}\n}\n\nfunc makeRes(c, m string) v1.ResourceList {\n\tcpu, _ := resource.ParseQuantity(c)\n\tmem, _ := resource.ParseQuantity(m)\n\n\treturn v1.ResourceList{\n\t\tv1.ResourceCPU:    cpu,\n\t\tv1.ResourceMemory: mem,\n\t}\n}\n"
  },
  {
    "path": "internal/client/switch_context_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage client\n\nimport (\n\t\"encoding/json\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync/atomic\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/apimachinery/pkg/util/cache\"\n\t\"k8s.io/apimachinery/pkg/version\"\n\t\"k8s.io/cli-runtime/pkg/genericclioptions\"\n)\n\nconst (\n\ttestContext1 = \"context1\"\n\ttestContext2 = \"context2\"\n)\n\nfunc newFakeK8sServer(t *testing.T) (*httptest.Server, *atomic.Int32) {\n\tt.Helper()\n\n\tversionCalls := &atomic.Int32{}\n\tmux := http.NewServeMux()\n\n\tmux.HandleFunc(\"/version\", func(w http.ResponseWriter, _ *http.Request) {\n\t\tversionCalls.Add(1)\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t_ = json.NewEncoder(w).Encode(version.Info{\n\t\t\tMajor:      \"1\",\n\t\t\tMinor:      \"28\",\n\t\t\tGitVersion: \"v1.28.0\",\n\t\t})\n\t})\n\n\tmux.HandleFunc(\"/api\", func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t_, _ = w.Write([]byte(`{\"kind\":\"APIVersions\",\"versions\":[\"v1\"]}`))\n\t})\n\n\tmux.HandleFunc(\"/apis\", func(w http.ResponseWriter, _ *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t_, _ = w.Write([]byte(`{\"kind\":\"APIGroupList\",\"apiVersion\":\"v1\",\"groups\":[]}`))\n\t})\n\n\treturn httptest.NewServer(mux), versionCalls\n}\n\nfunc writeSwitchTestKubeconfig(t *testing.T, server1URL, server2URL string) string {\n\tt.Helper()\n\n\tdir := t.TempDir()\n\tkubeconfig := filepath.Join(dir, \"config\")\n\tcontent := `apiVersion: v1\nkind: Config\ncurrent-context: context1\ncontexts:\n- context:\n    cluster: cluster1\n    user: user1\n    namespace: default\n  name: context1\n- context:\n    cluster: cluster2\n    user: user1\n    namespace: kube-system\n  name: context2\nclusters:\n- cluster:\n    server: ` + server1URL + `\n  name: cluster1\n- cluster:\n    server: ` + server2URL + `\n  name: cluster2\nusers:\n- name: user1\n  user:\n    token: test-token\n`\n\trequire.NoError(t, os.WriteFile(kubeconfig, []byte(content), 0600))\n\n\treturn kubeconfig\n}\n\nfunc setupSwitchTest(t *testing.T) (*httptest.Server, *atomic.Int32, *APIClient) {\n\tt.Helper()\n\n\tsrv, versionCalls := newFakeK8sServer(t)\n\tt.Cleanup(srv.Close)\n\tt.Setenv(\"HOME\", t.TempDir())\n\n\tkubeconfig := writeSwitchTestKubeconfig(t, srv.URL, srv.URL)\n\tflags := genericclioptions.NewConfigFlags(false)\n\tflags.KubeConfig = &kubeconfig\n\tctx := testContext1\n\tflags.Context = &ctx\n\n\ta := &APIClient{\n\t\tconfig: NewConfig(flags),\n\t\tcache:  cache.NewLRUExpireCache(cacheSize),\n\t\tconnOK: true,\n\t\tlog:    slog.Default(),\n\t}\n\n\treturn srv, versionCalls, a\n}\n\nfunc TestSwitchContextSuccess(t *testing.T) {\n\t_, _, a := setupSwitchTest(t)\n\n\terr := a.SwitchContext(testContext2)\n\trequire.NoError(t, err)\n\n\tctx, err := a.config.CurrentContextName()\n\trequire.NoError(t, err)\n\tassert.Equal(t, testContext2, ctx)\n\tassert.True(t, a.getConnOK())\n}\n\nfunc TestSwitchContextReusesConnectivityClient(t *testing.T) {\n\t_, _, a := setupSwitchTest(t)\n\n\terr := a.SwitchContext(testContext2)\n\trequire.NoError(t, err)\n\n\tassert.NotNil(t, a.getClient(),\n\t\t\"SwitchContext should store the connectivity-check client for reuse by Dial()\")\n}\n\nfunc TestSwitchContextPreWarmsDynDial(t *testing.T) {\n\t_, _, a := setupSwitchTest(t)\n\n\terr := a.SwitchContext(testContext2)\n\trequire.NoError(t, err)\n\n\tassert.NotNil(t, a.getDClient(),\n\t\t\"SwitchContext should pre-warm the dynamic client so gotoResource reuses it\")\n}\n\nfunc TestSwitchContextDialAfterSwitch(t *testing.T) {\n\t_, _, a := setupSwitchTest(t)\n\n\terr := a.SwitchContext(testContext2)\n\trequire.NoError(t, err)\n\n\tstoredClient := a.getClient()\n\trequire.NotNil(t, storedClient, \"SwitchContext should store client\")\n\n\tdialedClient, err := a.Dial()\n\trequire.NoError(t, err)\n\tassert.Same(t, storedClient, dialedClient,\n\t\t\"Dial() should return the stored connectivity client, not create a new one\")\n}\n\nfunc TestSwitchContextMinimalVersionCalls(t *testing.T) {\n\t_, versionCalls, a := setupSwitchTest(t)\n\n\terr := a.SwitchContext(testContext2)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, int32(1), versionCalls.Load(),\n\t\t\"SwitchContext should call ServerVersion exactly once\")\n}\n\nfunc TestSwitchContextInvalidContext(t *testing.T) {\n\t_, _, a := setupSwitchTest(t)\n\n\terr := a.SwitchContext(\"nonexistent\")\n\tassert.Error(t, err)\n}\n\nfunc TestInitConnectionMetricsUnsupported(t *testing.T) {\n\tsrv, _, _ := setupSwitchTest(t)\n\n\tkubeconfig := writeSwitchTestKubeconfig(t, srv.URL, srv.URL)\n\tflags := genericclioptions.NewConfigFlags(false)\n\tflags.KubeConfig = &kubeconfig\n\tctx := testContext1\n\tflags.Context = &ctx\n\n\ta, err := InitConnection(NewConfig(flags), slog.Default())\n\trequire.NoError(t, err)\n\tassert.True(t, a.ConnectionOK(),\n\t\t\"InitConnection should succeed when metrics-server is absent\")\n}\n\nfunc TestInitConnectionStoresDialClient(t *testing.T) {\n\tsrv, _, _ := setupSwitchTest(t)\n\n\tkubeconfig := writeSwitchTestKubeconfig(t, srv.URL, srv.URL)\n\tflags := genericclioptions.NewConfigFlags(false)\n\tflags.KubeConfig = &kubeconfig\n\tctx := testContext1\n\tflags.Context = &ctx\n\n\ta, err := InitConnection(NewConfig(flags), slog.Default())\n\trequire.NoError(t, err)\n\n\tassert.NotNil(t, a.getClient(),\n\t\t\"InitConnection should store a Dial client for reuse\")\n}\n"
  },
  {
    "path": "internal/client/testdata/config",
    "content": "apiVersion: v1\nkind: Config\npreferences: {}\nclusters:\n  - cluster:\n      insecure-skip-tls-verify: true\n      server: https://localhost:3000\n    name: fred\n  - cluster:\n      insecure-skip-tls-verify: true\n      server: https://localhost:3001\n    name: blee\n  - cluster:\n      insecure-skip-tls-verify: true\n      server: https://localhost:3002\n    name: zorg\ncontexts:\n  - context:\n      cluster: zorg\n      user: fred\n    name: fred\n  - context:\n      cluster: blee\n      user: blee\n      namespace: zorg\n    name: blee\n  - context:\n      cluster: duh\n      user: duh\n    name: duh\ncurrent-context: fred\nusers:\n  - name: fred\n    user:\n      client-certificate-data: ZnJlZA==\n      client-key-data: ZnJlZA==\n  - name: blee\n    user:\n      client-certificate-data: ZnJlZA==\n      client-key-data: ZnJlZA==\n  - name: duh\n    user:\n      client-certificate-data: ZnJlZA==\n      client-key-data: ZnJlZA==\n"
  },
  {
    "path": "internal/client/testdata/config.1",
    "content": "apiVersion: v1\nclusters:\n- cluster:\n    insecure-skip-tls-verify: true\n    server: https://localhost:3001\n  name: blee\n- cluster:\n    insecure-skip-tls-verify: true\n    server: https://localhost:3002\n  name: fred\ncontexts:\n- context:\n    cluster: blee\n    user: blee\n  name: blee\ncurrent-context: blee\nkind: Config\nusers: null\n"
  },
  {
    "path": "internal/client/testdata/config.2",
    "content": "apiVersion: v1\nclusters:\n- cluster:\n    insecure-skip-tls-verify: true\n    server: https://localhost:3001\n  name: blee\n- cluster:\n    insecure-skip-tls-verify: true\n    server: https://localhost:3002\n  name: fred\ncontexts:\n- context:\n    cluster: blee\n    user: blee\n  name: blee\n- context:\n    cluster: fred\n    user: fred\n  name: fred\ncurrent-context: blee\nkind: Config\npreferences: {}\nusers: null\n"
  },
  {
    "path": "internal/client/types.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage client\n\nimport (\n\t\"k8s.io/apimachinery/pkg/version\"\n\t\"k8s.io/client-go/discovery/cached/disk\"\n\t\"k8s.io/client-go/dynamic\"\n\t\"k8s.io/client-go/kubernetes\"\n\trestclient \"k8s.io/client-go/rest\"\n\tmv1beta1 \"k8s.io/metrics/pkg/apis/metrics/v1beta1\"\n\tversioned \"k8s.io/metrics/pkg/client/clientset/versioned\"\n)\n\nconst (\n\t// NA Not available.\n\tNA = \"n/a\"\n\n\t// NamespaceAll designates the fictional all namespace.\n\tNamespaceAll = \"all\"\n\n\t// BlankNamespace designates no namespace.\n\tBlankNamespace = \"\"\n\n\t// DefaultNamespace designates the default namespace.\n\tDefaultNamespace = \"default\"\n\n\t// ClusterScope designates a resource is not namespaced.\n\tClusterScope = \"-\"\n\n\t// NotNamespaced designates a non resource namespace.\n\tNotNamespaced = \"*\"\n\n\t// CreateVerb represents create access on a resource.\n\tCreateVerb = \"create\"\n\n\t// UpdateVerb represents an update access on a resource.\n\tUpdateVerb = \"update\"\n\n\t// PatchVerb represents a patch access on a resource.\n\tPatchVerb = \"patch\"\n\n\t// DeleteVerb represents a delete access on a resource.\n\tDeleteVerb = \"delete\"\n\n\t// GetVerb represents a get access on a resource.\n\tGetVerb = \"get\"\n\n\t// ListVerb represents a list access on a resource.\n\tListVerb = \"list\"\n\n\t// WatchVerb represents a watch access on a resource.\n\tWatchVerb = \"watch\"\n)\n\nvar (\n\t// PatchAccess patch a resource.\n\tPatchAccess = []string{PatchVerb}\n\n\t// GetAccess reads a resource.\n\tGetAccess = []string{GetVerb}\n\n\t// ListAccess list resources.\n\tListAccess = []string{ListVerb}\n\n\t// MonitorAccess monitors a collection of resources.\n\tMonitorAccess = []string{ListVerb, WatchVerb}\n\n\t// ReadAllAccess represents an all read access to a resource.\n\tReadAllAccess = []string{GetVerb, ListVerb, WatchVerb}\n)\n\n// ContainersMetrics tracks containers metrics.\ntype ContainersMetrics map[string]*mv1beta1.ContainerMetrics\n\n// NodesMetricsMap tracks node metrics.\ntype NodesMetricsMap map[string]*mv1beta1.NodeMetrics\n\n// PodsMetricsMap tracks pod metrics.\ntype PodsMetricsMap map[string]*mv1beta1.PodMetrics\n\n// Authorizer checks what a user can or cannot do to a resource.\ntype Authorizer interface {\n\t// CanI returns true if the user can use these actions for a given resource.\n\tCanI(ns string, gvr *GVR, n string, verbs []string) (bool, error)\n}\n\n// Connection represents a Kubernetes apiserver connection.\ntype Connection interface {\n\tAuthorizer\n\n\t// Config returns current config.\n\tConfig() *Config\n\n\t// ConnectionOK checks api server connection status.\n\tConnectionOK() bool\n\n\t// Dial connects to api server.\n\tDial() (kubernetes.Interface, error)\n\n\t// DialLogs connects to api server for logs.\n\tDialLogs() (kubernetes.Interface, error)\n\n\t// SwitchContext switches cluster based on context.\n\tSwitchContext(ctx string) error\n\n\t// CachedDiscovery connects to discovery client.\n\tCachedDiscovery() (*disk.CachedDiscoveryClient, error)\n\n\t// RestConfig connects to rest client.\n\tRestConfig() (*restclient.Config, error)\n\n\t// MXDial connects to metrics server.\n\tMXDial() (*versioned.Clientset, error)\n\n\t// DynDial connects to dynamic client.\n\tDynDial() (dynamic.Interface, error)\n\n\t// HasMetrics checks if metrics server is available.\n\tHasMetrics() bool\n\n\t// ValidNamespaceNames returns all available namespace names.\n\tValidNamespaceNames() (NamespaceNames, error)\n\n\t// IsValidNamespace checks if given namespace is known.\n\tIsValidNamespace(string) bool\n\n\t// ServerVersion returns current server version.\n\tServerVersion() (*version.Info, error)\n\n\t// CheckConnectivity checks if api server connection is happy or not.\n\tCheckConnectivity() bool\n\n\t// ActiveContext returns the current context name.\n\tActiveContext() string\n\n\t// ActiveNamespace returns the current namespace.\n\tActiveNamespace() string\n\n\t// IsActiveNamespace checks if given ns is active.\n\tIsActiveNamespace(string) bool\n}\n\n// CurrentMetrics tracks current cpu/mem.\ntype CurrentMetrics struct {\n\tCurrentCPU, CurrentMEM, CurrentEphemeral int64\n}\n\n// PodMetrics represent an aggregation of all pod containers metrics.\ntype PodMetrics CurrentMetrics\n\n// NodeMetrics describes raw node metrics.\ntype NodeMetrics struct {\n\tCurrentMetrics\n\n\tAllocatableCPU, AllocatableMEM, AllocatableEphemeral int64\n\tAvailableCPU, AvailableMEM, AvailableEphemeral       int64\n\tTotalCPU, TotalMEM, TotalEphemeral                   int64\n}\n\n// ClusterMetrics summarizes total node metrics as percentages.\ntype ClusterMetrics struct {\n\tPercCPU, PercMEM, PercEphemeral int\n}\n\n// NodesMetrics tracks usage metrics per nodes.\ntype NodesMetrics map[string]NodeMetrics\n\n// PodsMetrics tracks usage metrics per pods.\ntype PodsMetrics map[string]PodMetrics\n"
  },
  {
    "path": "internal/color/colorize.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage color\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n)\n\nconst colorFmt = \"\\x1b[%dm%s\\x1b[0m\"\n\n// Paint describes a terminal color.\ntype Paint int\n\n// Defines basic ANSI colors.\nconst (\n\tBlack     Paint = iota + 30 // 30\n\tRed                         // 31\n\tGreen                       // 32\n\tYellow                      // 33\n\tBlue                        // 34\n\tMagenta                     // 35\n\tCyan                        // 36\n\tLightGray                   // 37\n\tDarkGray  = 90\n\n\tBold = 1\n)\n\n// Colorize returns an ASCII colored string based on given color.\nfunc Colorize(s string, c Paint) string {\n\tif c == 0 {\n\t\treturn s\n\t}\n\treturn fmt.Sprintf(colorFmt, c, s)\n}\n\n// ANSIColorize colors a string.\nfunc ANSIColorize(text string, color int) string {\n\treturn \"\\033[38;5;\" + strconv.Itoa(color) + \"m\" + text + \"\\033[0m\"\n}\n\n// Highlight colorize bytes at given indices.\nfunc Highlight(bb []byte, ii []int, c int) []byte {\n\tif len(ii) == 0 {\n\t\treturn bb\n\t}\n\n\tresult := make([]byte, 0, len(bb)+len(ii)*20) // Extra space for color codes\n\n\t// Create a map of byte positions that should be highlighted\n\thighlightMap := make(map[int]bool)\n\tfor _, pos := range ii {\n\t\thighlightMap[pos] = true\n\t}\n\n\t// Process each byte\n\tfor i := 0; i < len(bb); i++ {\n\t\tif highlightMap[i] {\n\t\t\t// Check if this is the start of a UTF-8 character\n\t\t\tif (bb[i] & 0xC0) != 0x80 {\n\t\t\t\t// This is the start of a character, find the end\n\t\t\t\tcharStart := i\n\t\t\t\tcharEnd := i + 1\n\t\t\t\tfor charEnd < len(bb) && (bb[charEnd]&0xC0) == 0x80 {\n\t\t\t\t\tcharEnd++\n\t\t\t\t}\n\t\t\t\t// Colorize the entire character\n\t\t\t\tchar := string(bb[charStart:charEnd])\n\t\t\t\tcolored := ANSIColorize(char, c)\n\t\t\t\tresult = append(result, []byte(colored)...)\n\t\t\t\ti = charEnd - 1 // Skip the rest of the character bytes\n\t\t\t} else {\n\t\t\t\t// This is a continuation byte, skip it (already handled)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t} else {\n\t\t\tresult = append(result, bb[i])\n\t\t}\n\t}\n\n\treturn result\n}\n"
  },
  {
    "path": "internal/color/colorize_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage color_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/color\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestColorize(t *testing.T) {\n\tuu := map[string]struct {\n\t\ts string\n\t\tc color.Paint\n\t\te string\n\t}{\n\t\t\"white\":   {\"blee\", color.LightGray, \"\\x1b[37mblee\\x1b[0m\"},\n\t\t\"black\":   {\"blee\", color.Black, \"\\x1b[30mblee\\x1b[0m\"},\n\t\t\"default\": {\"blee\", 0, \"blee\"},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, color.Colorize(u.s, u.c))\n\t\t})\n\t}\n}\n\nfunc TestHighlight(t *testing.T) {\n\tuu := map[string]struct {\n\t\ttext    []byte\n\t\tindices []int\n\t\tcolor   int\n\t\te       string\n\t}{\n\t\t\"white\": {\n\t\t\ttext:    []byte(\"the brown fox\"),\n\t\t\tcolor:   209,\n\t\t\tindices: []int{4, 5, 6, 7, 8},\n\t\t\te:       \"the \\x1b[38;5;209mb\\x1b[0m\\x1b[38;5;209mr\\x1b[0m\\x1b[38;5;209mo\\x1b[0m\\x1b[38;5;209mw\\x1b[0m\\x1b[38;5;209mn\\x1b[0m fox\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, string(color.Highlight(u.text, u.indices, u.color)))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/config/alias.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nimport (\n\t\"errors\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/config/json\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n\t\"gopkg.in/yaml.v3\"\n\t\"k8s.io/apimachinery/pkg/util/sets\"\n)\n\ntype (\n\t// Alias tracks shortname to GVR mappings.\n\tAlias map[string]*client.GVR\n\n\t// ShortNames represents a collection of shortnames for aliases.\n\tShortNames map[*client.GVR][]string\n\n\t// Aliases represents a collection of aliases.\n\tAliases struct {\n\t\tAlias Alias `yaml:\"aliases\"`\n\t\tmx    sync.RWMutex\n\t}\n)\n\n// NewAliases return a new alias.\nfunc NewAliases() *Aliases {\n\treturn &Aliases{\n\t\tAlias: make(Alias, 50),\n\t}\n}\n\nfunc (a *Aliases) AliasesFor(gvr *client.GVR) sets.Set[string] {\n\ta.mx.RLock()\n\tdefer a.mx.RUnlock()\n\n\tss := sets.New[string]()\n\tfor alias, aliasGVR := range a.Alias {\n\t\tif aliasGVR == gvr {\n\t\t\tss.Insert(alias)\n\t\t}\n\t}\n\n\treturn ss\n}\n\n// ShortNames return all shortnames.\nfunc (a *Aliases) ShortNames() ShortNames {\n\ta.mx.RLock()\n\tdefer a.mx.RUnlock()\n\n\tm := make(ShortNames, len(a.Alias))\n\tfor alias, gvr := range a.Alias {\n\t\tif v, ok := m[gvr]; ok {\n\t\t\tm[gvr] = append(v, alias)\n\t\t} else {\n\t\t\tm[gvr] = []string{alias}\n\t\t}\n\t}\n\n\treturn m\n}\n\n// Clear remove all aliases.\nfunc (a *Aliases) Clear() {\n\ta.mx.Lock()\n\tdefer a.mx.Unlock()\n\n\tfor k := range a.Alias {\n\t\tdelete(a.Alias, k)\n\t}\n}\n\nfunc (a *Aliases) Resolve(p *cmd.Interpreter) (*client.GVR, bool) {\n\tgvr, ok := a.Get(p.Cmd())\n\tif !ok {\n\t\treturn nil, false\n\t}\n\n\tif gvr.IsK8sRes() {\n\t\tp.Reset(strings.Replace(p.GetLine(), p.Cmd(), gvr.String(), 1), p.Cmd())\n\t\treturn gvr, true\n\t}\n\n\tfor gvr.IsAlias() {\n\t\tap := cmd.NewInterpreter(gvr.String())\n\t\tgvr, ok = a.Get(ap.Cmd())\n\t\tif !ok {\n\t\t\treturn gvr, false\n\t\t}\n\t\tap.Merge(p)\n\t\tp.Reset(strings.Replace(ap.GetLine(), ap.Cmd(), gvr.String(), 1), ap.Cmd())\n\t}\n\n\treturn gvr, true\n}\n\n// Get retrieves an alias.\nfunc (a *Aliases) Get(alias string) (*client.GVR, bool) {\n\ta.mx.RLock()\n\tdefer a.mx.RUnlock()\n\n\tgvr, ok := a.Alias[alias]\n\n\treturn gvr, ok\n}\n\n// Define declares a new alias.\nfunc (a *Aliases) Define(gvr *client.GVR, aliases ...string) {\n\ta.mx.Lock()\n\tdefer a.mx.Unlock()\n\n\tfor _, alias := range aliases {\n\t\tif _, ok := a.Alias[alias]; !ok && alias != \"\" {\n\t\t\ta.Alias[alias] = gvr\n\t\t}\n\t}\n}\n\n// Load K9s aliases.\nfunc (a *Aliases) Load(path string) error {\n\ta.loadDefaultAliases()\n\tf, err := EnsureAliasesCfgFile()\n\tif err != nil {\n\t\tslog.Error(\"Unable to gen config aliases\", slogs.Error, err)\n\t}\n\t// load global alias file\n\tif err := a.LoadFile(f); err != nil {\n\t\treturn err\n\t}\n\n\t// load context specific aliases if any\n\treturn a.LoadFile(path)\n}\n\n// LoadFile loads alias from a given file.\nfunc (a *Aliases) LoadFile(path string) error {\n\tif _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {\n\t\treturn nil\n\t}\n\n\tbb, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := data.JSONValidator.Validate(json.AliasesSchema, bb); err != nil {\n\t\tslog.Warn(\"Aliases validation failed\", slogs.Error, err)\n\t}\n\n\ta.mx.Lock()\n\tif err := yaml.Unmarshal(bb, a); err != nil {\n\t\treturn err\n\t}\n\n\tfor k, v := range a.Alias {\n\t\ta.Alias[k] = client.NewGVR(v.String())\n\t}\n\tdefer a.mx.Unlock()\n\n\treturn nil\n}\n\nfunc (a *Aliases) declare(gvr *client.GVR, aliases ...string) {\n\ta.Alias[gvr.String()] = gvr\n\tfor _, alias := range aliases {\n\t\ta.Alias[alias] = gvr\n\t}\n}\n\nfunc (a *Aliases) loadDefaultAliases() {\n\ta.mx.Lock()\n\tdefer a.mx.Unlock()\n\n\ta.declare(client.HlpGVR, \"h\", \"?\")\n\ta.declare(client.QGVR, \"q\", \"q!\", \"qa\", \"Q\")\n\ta.declare(client.AliGVR, \"alias\", \"a\")\n\ta.declare(client.HmGVR, \"charts\", \"chart\", \"hm\")\n\ta.declare(client.DirGVR, \"dir\", \"d\")\n\ta.declare(client.CtGVR, \"context\", \"ctx\")\n\ta.declare(client.UsrGVR, \"user\", \"usr\")\n\ta.declare(client.GrpGVR, \"group\", \"grp\")\n\ta.declare(client.PfGVR, \"portforward\", \"pf\")\n\ta.declare(client.BeGVR, \"benchmark\", \"bench\")\n\ta.declare(client.SdGVR, \"screendump\", \"sd\")\n\ta.declare(client.PuGVR, \"pulse\", \"pu\", \"hz\")\n\ta.declare(client.XGVR, \"xray\", \"x\")\n\ta.declare(client.WkGVR, \"workload\", \"wk\")\n}\n\n// Save alias to disk.\nfunc (a *Aliases) Save() error {\n\tslog.Debug(\"Saving Aliases...\")\n\ta.mx.RLock()\n\tdefer a.mx.RUnlock()\n\n\treturn a.saveAliases(AppAliasesFile)\n}\n\n// SaveAliases saves aliases to a given file.\nfunc (a *Aliases) saveAliases(path string) error {\n\tif err := data.EnsureDirPath(path, data.DefaultDirMod); err != nil {\n\t\treturn err\n\t}\n\n\treturn data.SaveYAML(path, a)\n}\n"
  },
  {
    "path": "internal/config/alias_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config_test\n\nimport (\n\t\"maps\"\n\t\"os\"\n\t\"path\"\n\t\"slices\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestAliasClear(t *testing.T) {\n\ta := testAliases()\n\ta.Clear()\n\n\tassert.Empty(t, slices.Collect(maps.Keys(a.Alias)))\n}\n\nfunc TestAliasKeys(t *testing.T) {\n\ta := testAliases()\n\tkk := maps.Keys(a.Alias)\n\n\tassert.Equal(t, []string{\"a1\", \"a11\", \"a2\", \"a3\"}, slices.Sorted(kk))\n}\n\nfunc TestAliasShortNames(t *testing.T) {\n\ta := testAliases()\n\tess := config.ShortNames{\n\t\tgvr1: []string{\"a1\", \"a11\"},\n\t\tgvr2: []string{\"a2\"},\n\t\tgvr3: []string{\"a3\"},\n\t}\n\tss := a.ShortNames()\n\tassert.Len(t, ss, len(ess))\n\tfor k, v := range ss {\n\t\tv1, ok := ess[k]\n\t\tassert.True(t, ok, \"missing: %q\", k)\n\t\tslices.Sort(v)\n\t\tassert.Equal(t, v1, v)\n\t}\n}\n\nfunc TestAliasDefine(t *testing.T) {\n\ttype aliasDef struct {\n\t\tgvr     *client.GVR\n\t\taliases []string\n\t}\n\n\tuu := map[string]struct {\n\t\taliases            []aliasDef\n\t\tregisteredCommands map[string]*client.GVR\n\t}{\n\t\t\"simple\": {\n\t\t\taliases: []aliasDef{\n\t\t\t\t{\n\t\t\t\t\tgvr:     client.NewGVR(\"one\"),\n\t\t\t\t\taliases: []string{\"blee\", \"duh\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tregisteredCommands: map[string]*client.GVR{\n\t\t\t\t\"blee\": client.NewGVR(\"one\"),\n\t\t\t\t\"duh\":  client.NewGVR(\"one\"),\n\t\t\t},\n\t\t},\n\t\t\"duplicates\": {\n\t\t\taliases: []aliasDef{\n\t\t\t\t{\n\t\t\t\t\tgvr:     client.NewGVR(\"one\"),\n\t\t\t\t\taliases: []string{\"blee\", \"duh\"},\n\t\t\t\t}, {\n\t\t\t\t\tgvr:     client.NewGVR(\"two\"),\n\t\t\t\t\taliases: []string{\"blee\", \"duh\", \"fred\", \"zorg\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tregisteredCommands: map[string]*client.GVR{\n\t\t\t\t\"blee\": client.NewGVR(\"one\"),\n\t\t\t\t\"duh\":  client.NewGVR(\"one\"),\n\t\t\t\t\"fred\": client.NewGVR(\"two\"),\n\t\t\t\t\"zorg\": client.NewGVR(\"two\"),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tconfigAlias := config.NewAliases()\n\t\t\tfor _, aliases := range u.aliases {\n\t\t\t\tfor _, a := range aliases.aliases {\n\t\t\t\t\tconfigAlias.Define(aliases.gvr, a)\n\t\t\t\t}\n\t\t\t}\n\t\t\tfor alias, cmd := range u.registeredCommands {\n\t\t\t\tv, ok := configAlias.Get(alias)\n\t\t\t\tassert.True(t, ok)\n\t\t\t\tassert.Equal(t, cmd, v, \"Wrong command for alias \"+alias)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAliasesLoad(t *testing.T) {\n\tconfig.AppConfigDir = \"testdata/aliases\"\n\ta := config.NewAliases()\n\trequire.NoError(t, a.Load(path.Join(config.AppConfigDir, \"plain.yaml\")))\n\n\tassert.Len(t, a.Alias, 55)\n}\n\nfunc TestAliasesSave(t *testing.T) {\n\trequire.NoError(t, data.EnsureFullPath(\"/tmp/test-aliases\", data.DefaultDirMod))\n\tdefer require.NoError(t, os.RemoveAll(\"/tmp/test-aliases\"))\n\n\tconfig.AppAliasesFile = \"/tmp/test-aliases/aliases.yaml\"\n\ta := testAliases()\n\tc := len(a.Alias)\n\n\tassert.Len(t, a.Alias, c)\n\trequire.NoError(t, a.Save())\n\trequire.NoError(t, a.LoadFile(config.AppAliasesFile))\n\tassert.Len(t, a.Alias, c)\n}\n\nfunc TestAliasResolve(t *testing.T) {\n\tuu := map[string]struct {\n\t\texp string\n\t\tok  bool\n\t\tgvr *client.GVR\n\t\tcmd *cmd.Interpreter\n\t}{\n\t\t\"gvr\": {\n\t\t\texp: \"v1/pods\",\n\t\t\tok:  true,\n\t\t\tgvr: client.PodGVR,\n\t\t\tcmd: cmd.NewInterpreter(\"v1/pods\"),\n\t\t},\n\n\t\t\"kind\": {\n\t\t\texp: \"pod\",\n\t\t\tok:  true,\n\t\t\tgvr: client.PodGVR,\n\t\t\tcmd: cmd.NewInterpreter(\"v1/pods\"),\n\t\t},\n\n\t\t\"plural\": {\n\t\t\texp: \"pods\",\n\t\t\tok:  true,\n\t\t\tgvr: client.PodGVR,\n\t\t\tcmd: cmd.NewInterpreter(\"v1/pods\"),\n\t\t},\n\n\t\t\"short-name\": {\n\t\t\texp: \"po\",\n\t\t\tok:  true,\n\t\t\tgvr: client.PodGVR,\n\t\t\tcmd: cmd.NewInterpreter(\"v1/pods\"),\n\t\t},\n\n\t\t\"short-name-with-args\": {\n\t\t\texp: \"po 'a in (b,c)' @zorb bozo\",\n\t\t\tok:  true,\n\t\t\tgvr: client.PodGVR,\n\t\t\tcmd: cmd.NewInterpreter(\"v1/pods 'a in (b,c)' @zorb bozo\"),\n\t\t},\n\n\t\t\"alias\": {\n\t\t\texp: \"pipo\",\n\t\t\tok:  true,\n\t\t\tgvr: client.PodGVR,\n\t\t\tcmd: cmd.NewInterpreter(\"v1/pods\"),\n\t\t},\n\n\t\t\"toast-command\": {\n\t\t\texp: \"zorg\",\n\t\t},\n\n\t\t\"alias-no-args\": {\n\t\t\texp: \"wkl\",\n\t\t\tok:  true,\n\t\t\tgvr: client.WkGVR,\n\t\t\tcmd: cmd.NewInterpreter(\"workloads\"),\n\t\t},\n\n\t\t\"alias-ns-arg\": {\n\t\t\texp: \"pp\",\n\t\t\tok:  true,\n\t\t\tgvr: client.PodGVR,\n\t\t\tcmd: cmd.NewInterpreter(\"v1/pods default\"),\n\t\t},\n\n\t\t\"multi-alias-ns-inception\": {\n\t\t\texp: \"ppo\",\n\t\t\tok:  true,\n\t\t\tgvr: client.PodGVR,\n\t\t\tcmd: cmd.NewInterpreter(\"v1/pods 'a=b,b=c' default\"),\n\t\t},\n\n\t\t\"full-alias\": {\n\t\t\texp: \"ppc\",\n\t\t\tok:  true,\n\t\t\tgvr: client.PodGVR,\n\t\t\tcmd: cmd.NewInterpreter(\"v1/pods @fred 'app=fred' default\"),\n\t\t},\n\n\t\t\"plain-filter\": {\n\t\t\texp: \"po /fred @bozo ns-1\",\n\t\t\tok:  true,\n\t\t\tgvr: client.PodGVR,\n\t\t\tcmd: cmd.NewInterpreter(\"v1/pods /fred @bozo ns-1\"),\n\t\t},\n\n\t\t\"alias-filter\": {\n\t\t\texp: \"pipo /fred @bozo ns-1\",\n\t\t\tok:  true,\n\t\t\tgvr: client.PodGVR,\n\t\t\tcmd: cmd.NewInterpreter(\"v1/pods /fred @bozo ns-1\"),\n\t\t},\n\n\t\t\"complex-filter\": {\n\t\t\texp: \"ppc /fred @bozo ns-1\",\n\t\t\tok:  true,\n\t\t\tgvr: client.PodGVR,\n\t\t\tcmd: cmd.NewInterpreter(\"v1/pods @bozo /fred 'app=fred' ns-1\"),\n\t\t},\n\n\t\t\"filtered\": {\n\t\t\texp: \"pc\",\n\t\t\tok:  true,\n\t\t\tgvr: client.PodGVR,\n\t\t\tcmd: cmd.NewInterpreter(\"v1/pods /cilium kube-system\"),\n\t\t},\n\n\t\t\"labels-in\": {\n\t\t\texp: \"ppp\",\n\t\t\tok:  true,\n\t\t\tgvr: client.PodGVR,\n\t\t\tcmd: cmd.NewInterpreter(\"v1/pods 'app in (be,fe)'\"),\n\t\t},\n\t}\n\n\ta := config.NewAliases()\n\ta.Define(client.PodGVR, \"po\", \"pipo\", \"pod\")\n\ta.Define(client.PodGVR, client.PodGVR.String())\n\ta.Define(client.PodGVR, client.PodGVR.AsResourceName())\n\ta.Define(client.WkGVR, client.WkGVR.String(), \"workload\", \"wkl\")\n\ta.Define(client.NewGVR(\"pod default\"), \"pp\")\n\ta.Define(client.NewGVR(\"pipo a=b,b=c default\"), \"ppo\")\n\ta.Define(client.NewGVR(\"pod default app=fred @fred\"), \"ppc\")\n\ta.Define(client.NewGVR(\"pod /cilium kube-system\"), \"pc\")\n\ta.Define(client.NewGVR(\"pod 'app in (be,fe)'\"), \"ppp\")\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tp := cmd.NewInterpreter(u.exp)\n\t\t\tgvr, ok := a.Resolve(p)\n\t\t\tassert.Equal(t, u.ok, ok)\n\t\t\tif ok {\n\t\t\t\tassert.Equal(t, u.gvr, gvr)\n\t\t\t\tassert.Equal(t, u.cmd.GetLine(), p.GetLine())\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Helpers...\n\nvar (\n\tgvr1 = client.NewGVR(\"gvr1\")\n\tgvr2 = client.NewGVR(\"gvr2\")\n\tgvr3 = client.NewGVR(\"gvr3\")\n)\n\nfunc testAliases() *config.Aliases {\n\ta := config.NewAliases()\n\ta.Alias[\"a1\"] = gvr1\n\ta.Alias[\"a11\"] = gvr1\n\ta.Alias[\"a2\"] = gvr2\n\ta.Alias[\"a3\"] = gvr3\n\n\treturn a\n}\n"
  },
  {
    "path": "internal/config/benchmark.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nimport (\n\t\"net/http\"\n\t\"os\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\n// K9sBench the name of the benchmarks config file.\nvar K9sBench = \"bench\"\n\ntype (\n\t// Bench tracks K9s styling options.\n\tBench struct {\n\t\tBenchmarks *Benchmarks `yaml:\"benchmarks\"`\n\t}\n\n\t// Benchmarks tracks K9s benchmarks configuration.\n\tBenchmarks struct {\n\t\tDefaults   Benchmark              `yaml:\"defaults\"`\n\t\tServices   map[string]BenchConfig `yam':\"services\"`\n\t\tContainers map[string]BenchConfig `yam':\"containers\"`\n\t}\n\n\t// Auth basic auth creds.\n\tAuth struct {\n\t\tUser     string `yaml:\"user\"`\n\t\tPassword string `yaml:\"password\"`\n\t}\n\n\t// Benchmark represents a generic benchmark.\n\tBenchmark struct {\n\t\tC int `yaml:\"concurrency\"`\n\t\tN int `yaml:\"requests\"`\n\t}\n\n\t// HTTP represents an http request.\n\tHTTP struct {\n\t\tMethod  string      `yaml:\"method\"`\n\t\tHost    string      `yaml:\"host\"`\n\t\tPath    string      `yaml:\"path\"`\n\t\tHTTP2   bool        `yaml:\"http2\"`\n\t\tBody    string      `yaml:\"body\"`\n\t\tHeaders http.Header `yaml:\"headers\"`\n\t}\n\n\t// BenchConfig represents a service benchmark.\n\tBenchConfig struct {\n\t\tName string\n\t\tC    int  `yaml:\"concurrency\"`\n\t\tN    int  `yaml:\"requests\"`\n\t\tAuth Auth `yaml:\"auth\"`\n\t\tHTTP HTTP `yaml:\"http\"`\n\t}\n)\n\nconst (\n\t// DefaultC default concurrency.\n\tDefaultC = 1\n\t// DefaultN default number of requests.\n\tDefaultN = 200\n\t// DefaultMethod default http verb.\n\tDefaultMethod = \"GET\"\n)\n\n// DefaultBenchSpec returns a default bench spec.\nfunc DefaultBenchSpec() BenchConfig {\n\treturn BenchConfig{\n\t\tC: DefaultC,\n\t\tN: DefaultN,\n\t\tHTTP: HTTP{\n\t\t\tMethod: DefaultMethod,\n\t\t\tPath:   \"/\",\n\t\t},\n\t}\n}\n\nfunc newBenchmark() Benchmark {\n\treturn Benchmark{\n\t\tC: DefaultC,\n\t\tN: DefaultN,\n\t}\n}\n\n// Empty checks if the benchmark is set.\nfunc (b Benchmark) Empty() bool {\n\treturn b.C == 0 && b.N == 0\n}\n\nfunc newBenchmarks() *Benchmarks {\n\treturn &Benchmarks{\n\t\tDefaults: newBenchmark(),\n\t}\n}\n\n// NewBench creates a new default config.\nfunc NewBench(path string) (*Bench, error) {\n\ts := &Bench{Benchmarks: newBenchmarks()}\n\terr := s.load(path)\n\treturn s, err\n}\n\n// Reload update the configuration from disk.\nfunc (s *Bench) Reload(path string) error {\n\treturn s.load(path)\n}\n\n// Load K9s benchmark configs from file.\nfunc (s *Bench) load(path string) error {\n\tf, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn yaml.Unmarshal(f, &s)\n}\n"
  },
  {
    "path": "internal/config/benchmark_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestBenchEmpty(t *testing.T) {\n\tuu := map[string]struct {\n\t\tb Benchmark\n\t\te bool\n\t}{\n\t\t\"empty\":    {Benchmark{}, true},\n\t\t\"notEmpty\": {newBenchmark(), false},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.b.Empty())\n\t\t})\n\t}\n}\n\nfunc TestBenchLoad(t *testing.T) {\n\tuu := map[string]struct {\n\t\tfile     string\n\t\tc, n     int\n\t\tsvcCount int\n\t\tcoCount  int\n\t}{\n\t\t\"goodConfig\": {\n\t\t\t\"testdata/benchmarks/b_good.yaml\",\n\t\t\t2,\n\t\t\t1000,\n\t\t\t2,\n\t\t\t0,\n\t\t},\n\t\t\"malformed\": {\n\t\t\t\"testdata/benchmarks/b_toast.yaml\",\n\t\t\t1,\n\t\t\t200,\n\t\t\t0,\n\t\t\t0,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tb, err := NewBench(u.file)\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, u.c, b.Benchmarks.Defaults.C)\n\t\t\tassert.Equal(t, u.n, b.Benchmarks.Defaults.N)\n\t\t\tassert.Len(t, b.Benchmarks.Services, u.svcCount)\n\t\t\tassert.Len(t, b.Benchmarks.Containers, u.coCount)\n\t\t})\n\t}\n}\n\nfunc TestBenchServiceLoad(t *testing.T) {\n\tuu := map[string]struct {\n\t\tkey                string\n\t\tc, n               int\n\t\tmethod, host, path string\n\t\thttp2              bool\n\t\tbody               string\n\t\tauth               Auth\n\t\theaders            http.Header\n\t}{\n\t\t\"s1\": {\n\t\t\t\"default/nginx\",\n\t\t\t2,\n\t\t\t1000,\n\t\t\t\"GET\",\n\t\t\t\"10.10.10.10\",\n\t\t\t\"/\",\n\t\t\ttrue,\n\t\t\t`{\"fred\": \"blee\"}`,\n\t\t\tAuth{\"fred\", \"blee\"},\n\t\t\thttp.Header{\"Accept\": []string{\"text/html\"}, \"Content-Type\": []string{\"application/json\"}},\n\t\t},\n\t\t\"s2\": {\n\t\t\t\"blee/fred\",\n\t\t\t10,\n\t\t\t1500,\n\t\t\t\"POST\",\n\t\t\t\"20.20.20.20\",\n\t\t\t\"/zorg\",\n\t\t\tfalse,\n\t\t\t`{\"fred\": \"blee\"}`,\n\t\t\tAuth{\"fred\", \"blee\"},\n\t\t\thttp.Header{\"Accept\": []string{\"text/html\"}, \"Content-Type\": []string{\"application/json\"}},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tb, err := NewBench(\"testdata/benchmarks/b_good.yaml\")\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Len(t, b.Benchmarks.Services, 2)\n\t\t\tsvc := b.Benchmarks.Services[u.key]\n\t\t\tassert.Equal(t, u.c, svc.C)\n\t\t\tassert.Equal(t, u.n, svc.N)\n\t\t\tassert.Equal(t, u.method, svc.HTTP.Method)\n\t\t\tassert.Equal(t, u.host, svc.HTTP.Host)\n\t\t\tassert.Equal(t, u.path, svc.HTTP.Path)\n\t\t\tassert.Equal(t, u.http2, svc.HTTP.HTTP2)\n\t\t\tassert.Equal(t, u.body, svc.HTTP.Body)\n\t\t\tassert.Equal(t, u.auth, svc.Auth)\n\t\t\tassert.Equal(t, u.headers, svc.HTTP.Headers)\n\t\t})\n\t}\n}\n\nfunc TestBenchReLoad(t *testing.T) {\n\tb, err := NewBench(\"testdata/benchmarks/b_containers.yaml\")\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, 2, b.Benchmarks.Defaults.C)\n\trequire.NoError(t, b.Reload(\"testdata/benchmarks/b_containers_1.yaml\"))\n\tassert.Equal(t, 20, b.Benchmarks.Defaults.C)\n}\n\nfunc TestBenchLoadToast(t *testing.T) {\n\t_, err := NewBench(\"testdata/toast.yaml\")\n\tassert.Error(t, err)\n}\n\nfunc TestBenchContainerLoad(t *testing.T) {\n\tuu := map[string]struct {\n\t\tkey                string\n\t\tc, n               int\n\t\tmethod, host, path string\n\t\thttp2              bool\n\t\tbody               string\n\t\tauth               Auth\n\t\theaders            http.Header\n\t}{\n\t\t\"c1\": {\n\t\t\t\"c1\",\n\t\t\t2,\n\t\t\t1000,\n\t\t\t\"GET\",\n\t\t\t\"10.10.10.10\",\n\t\t\t\"/duh\",\n\t\t\ttrue,\n\t\t\t`{\"fred\": \"blee\"}`,\n\t\t\tAuth{\"fred\", \"blee\"},\n\t\t\thttp.Header{\"Accept\": []string{\"text/html\"}, \"Content-Type\": []string{\"application/json\"}},\n\t\t},\n\t\t\"c2\": {\n\t\t\t\"c2\",\n\t\t\t10,\n\t\t\t1500,\n\t\t\t\"POST\",\n\t\t\t\"20.20.20.20\",\n\t\t\t\"/fred\",\n\t\t\tfalse,\n\t\t\t`{\"fred\": \"blee\"}`,\n\t\t\tAuth{\"fred\", \"blee\"},\n\t\t\thttp.Header{\"Accept\": []string{\"text/html\"}, \"Content-Type\": []string{\"application/json\"}},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tb, err := NewBench(\"testdata/benchmarks/b_containers.yaml\")\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Len(t, b.Benchmarks.Services, 2)\n\t\t\tco := b.Benchmarks.Containers[u.key]\n\t\t\tassert.Equal(t, u.c, co.C)\n\t\t\tassert.Equal(t, u.n, co.N)\n\t\t\tassert.Equal(t, u.method, co.HTTP.Method)\n\t\t\tassert.Equal(t, u.host, co.HTTP.Host)\n\t\t\tassert.Equal(t, u.path, co.HTTP.Path)\n\t\t\tassert.Equal(t, u.http2, co.HTTP.HTTP2)\n\t\t\tassert.Equal(t, u.body, co.HTTP.Body)\n\t\t\tassert.Equal(t, u.auth, co.Auth)\n\t\t\tassert.Equal(t, u.headers, co.HTTP.Headers)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/config/color.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/lucasb-eyer/go-colorful\"\n)\n\nconst (\n\t// DefaultColor represents  a default color.\n\tDefaultColor Color = \"default\"\n\n\t// TransparentColor represents the terminal bg color.\n\tTransparentColor Color = \"-\"\n)\n\n// Colors tracks multiple colors.\ntype Colors []Color\n\n// Colors converts series string colors to colors.\nfunc (c Colors) Colors() []tcell.Color {\n\tcc := make([]tcell.Color, 0, len(c))\n\tfor _, color := range c {\n\t\tcc = append(cc, color.Color())\n\t}\n\n\treturn cc\n}\n\n// Invert returns a new Colors with all colors inverted.\nfunc (c Colors) Invert() Colors {\n\tinverted := make(Colors, len(c))\n\tfor i, color := range c {\n\t\tinverted[i] = color.InvertColor()\n\t}\n\treturn inverted\n}\n\n// Color represents a color.\ntype Color string\n\n// NewColor returns a new color.\nfunc NewColor(c string) Color {\n\treturn Color(c)\n}\n\n// String returns color as string.\nfunc (c Color) String() string {\n\tif c.isHex() {\n\t\treturn string(c)\n\t}\n\tif c == DefaultColor {\n\t\treturn \"-\"\n\t}\n\tcol := c.Color().TrueColor().Hex()\n\tif col < 0 {\n\t\treturn \"-\"\n\t}\n\n\treturn fmt.Sprintf(\"#%06x\", col)\n}\n\nfunc (c Color) isHex() bool {\n\treturn len(c) == 7 && c[0] == '#'\n}\n\n// Color returns a view color.\nfunc (c Color) Color() tcell.Color {\n\tif c == DefaultColor {\n\t\treturn tcell.ColorDefault\n\t}\n\n\treturn tcell.GetColor(string(c)).TrueColor()\n}\n\n// maxChromaForLH finds the maximum chroma at a given lightness and hue\n// that stays within the sRGB gamut using binary search.\nfunc maxChromaForLH(l, h float64) float64 {\n\tlo, hi := 0.0, 0.4\n\tfor hi-lo > 0.001 {\n\t\tmid := (lo + hi) / 2\n\t\tcol := colorful.OkLch(l, mid, h)\n\t\tif col.IsValid() {\n\t\t\tlo = mid\n\t\t} else {\n\t\t\thi = mid\n\t\t}\n\t}\n\treturn lo\n}\n\n// chromaPreserveFactor controls how much original chroma to preserve during\n// inversion. 0.5 means we try to keep 50% of the original chroma, which\n// provides a good balance between color differentiation and L inversion.\nconst chromaPreserveFactor = 0.5\n\n// closestLForChroma finds the L value closest to targetL that can support\n// the given chroma at the given hue. It searches toward 0.5 first (where\n// gamut is typically larger), then away from 0.5 if needed.\nfunc closestLForChroma(targetL, c, h float64) float64 {\n\tif maxChromaForLH(targetL, h) >= c {\n\t\treturn targetL\n\t}\n\n\t// Search toward 0.5 first (where gamut is larger)\n\tif targetL < 0.5 {\n\t\tfor ll := targetL; ll <= 0.5; ll += 0.01 {\n\t\t\tif maxChromaForLH(ll, h) >= c {\n\t\t\t\treturn ll\n\t\t\t}\n\t\t}\n\t\t// Continue searching above 0.5 if needed\n\t\tfor ll := 0.51; ll <= 0.95; ll += 0.01 {\n\t\t\tif maxChromaForLH(ll, h) >= c {\n\t\t\t\treturn ll\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor ll := targetL; ll >= 0.5; ll -= 0.01 {\n\t\t\tif maxChromaForLH(ll, h) >= c {\n\t\t\t\treturn ll\n\t\t\t}\n\t\t}\n\t\t// Continue searching below 0.5 if needed\n\t\tfor ll := 0.49; ll >= 0.05; ll -= 0.01 {\n\t\t\tif maxChromaForLH(ll, h) >= c {\n\t\t\t\treturn ll\n\t\t\t}\n\t\t}\n\t}\n\n\treturn targetL\n}\n\n// InvertColor inverts the color's lightness in Oklch space while preserving\n// chroma (saturation). For chromatic colors, L is adjusted toward 0.5 only\n// as needed to preserve a fraction of the original chroma (set by\n// chromaPreserveFactor), since the sRGB gamut has less room for chroma at\n// extreme lightness values.\n// Special colors (default, transparent) are returned unchanged.\nfunc (c Color) InvertColor() Color {\n\tif c == DefaultColor || c == TransparentColor || c == \"\" {\n\t\treturn c\n\t}\n\n\ttc := c.Color()\n\tif tc == tcell.ColorDefault {\n\t\treturn c\n\t}\n\n\thex := tc.TrueColor().Hex()\n\tif hex < 0 {\n\t\treturn c\n\t}\n\n\tcol, err := colorful.Hex(fmt.Sprintf(\"#%06x\", hex))\n\tif err != nil {\n\t\treturn c\n\t}\n\n\tL, C, h := col.OkLch()\n\n\t// For achromatic colors, simply invert L\n\tif C < 0.01 {\n\t\treturn NewColor(colorful.OkLch(1.0-L, 0, h).Clamped().Hex())\n\t}\n\n\t// For chromatic colors, find L closest to inverted that preserves\n\t// at least chromaPreserveFactor of the original chroma\n\ttargetL := 1.0 - L\n\tminC := C * chromaPreserveFactor\n\tactualL := closestLForChroma(targetL, minC, h)\n\n\t// Use as much of the original chroma as the gamut allows at actualL\n\tmaxC := maxChromaForLH(actualL, h)\n\tactualC := C\n\tif maxC < C {\n\t\tactualC = maxC\n\t}\n\n\tinverted := colorful.OkLch(actualL, actualC, h).Clamped()\n\n\treturn NewColor(inverted.Hex())\n}\n"
  },
  {
    "path": "internal/config/color_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config_test\n\nimport (\n\t\"math\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/lucasb-eyer/go-colorful\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestColors(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcc []string\n\t\tee []tcell.Color\n\t}{\n\t\t\"empty\": {\n\t\t\tee: []tcell.Color{},\n\t\t},\n\t\t\"default\": {\n\t\t\tcc: []string{\"default\"},\n\t\t\tee: []tcell.Color{tcell.ColorDefault},\n\t\t},\n\t\t\"multi\": {\n\t\t\tcc: []string{\n\t\t\t\t\"default\",\n\t\t\t\t\"transparent\",\n\t\t\t\t\"blue\",\n\t\t\t\t\"green\",\n\t\t\t},\n\t\t\tee: []tcell.Color{\n\t\t\t\ttcell.ColorDefault,\n\t\t\t\ttcell.ColorDefault,\n\t\t\t\ttcell.ColorBlue.TrueColor(),\n\t\t\t\ttcell.ColorGreen.TrueColor(),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tcc := make(config.Colors, 0, len(u.cc))\n\t\t\tfor _, c := range u.cc {\n\t\t\t\tcc = append(cc, config.NewColor(c))\n\t\t\t}\n\t\t\tassert.Equal(t, u.ee, cc.Colors())\n\t\t})\n\t}\n}\n\nfunc TestColorString(t *testing.T) {\n\tuu := map[string]struct {\n\t\tc string\n\t\te string\n\t}{\n\t\t\"empty\": {\n\t\t\te: \"-\",\n\t\t},\n\t\t\"default\": {\n\t\t\tc: \"default\",\n\t\t\te: \"-\",\n\t\t},\n\t\t\"transparent\": {\n\t\t\tc: \"-\",\n\t\t\te: \"-\",\n\t\t},\n\t\t\"blue\": {\n\t\t\tc: \"blue\",\n\t\t\te: \"#0000ff\",\n\t\t},\n\t\t\"lightgray\": {\n\t\t\tc: \"lightgray\",\n\t\t\te: \"#d3d3d3\",\n\t\t},\n\t\t\"hex\": {\n\t\t\tc: \"#00ff00\",\n\t\t\te: \"#00ff00\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tc := config.NewColor(u.c)\n\t\t\tassert.Equal(t, u.e, c.String())\n\t\t})\n\t}\n}\n\nfunc TestColorToColor(t *testing.T) {\n\tuu := map[string]struct {\n\t\tc string\n\t\te tcell.Color\n\t}{\n\t\t\"default\": {\n\t\t\tc: \"default\",\n\t\t\te: tcell.ColorDefault,\n\t\t},\n\t\t\"transparent\": {\n\t\t\tc: \"-\",\n\t\t\te: tcell.ColorDefault,\n\t\t},\n\t\t\"aqua\": {\n\t\t\tc: \"aqua\",\n\t\t\te: tcell.ColorAqua.TrueColor(),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tc := config.NewColor(u.c)\n\t\t\tassert.Equal(t, u.e, c.Color())\n\t\t})\n\t}\n}\n\n// getOkch returns c, h for a hex color string.\nfunc getOkch(hex string) (c, h float64) {\n\tcol, err := colorful.Hex(hex)\n\tif err != nil {\n\t\treturn 0, 0\n\t}\n\t_, c, h = col.OkLch()\n\treturn c, h\n}\n\n// huesEqual checks if two hues are equal within tolerance, handling wraparound.\nfunc huesEqual(h1, h2, tolerance float64) bool {\n\tdiff := math.Abs(h1 - h2)\n\tif diff > 180 {\n\t\tdiff = 360 - diff\n\t}\n\treturn diff < tolerance\n}\n\nfunc TestInvertColor(t *testing.T) {\n\tuu := map[string]struct {\n\t\tc      string\n\t\texpect string\n\t}{\n\t\t\"default\": {\n\t\t\tc:      \"default\",\n\t\t\texpect: \"default\",\n\t\t},\n\t\t\"transparent\": {\n\t\t\tc:      \"-\",\n\t\t\texpect: \"-\",\n\t\t},\n\t\t\"empty\": {\n\t\t\tc:      \"\",\n\t\t\texpect: \"\",\n\t\t},\n\t\t\"black_to_white\": {\n\t\t\tc:      \"#000000\",\n\t\t\texpect: \"#ffffff\",\n\t\t},\n\t\t\"white_to_black\": {\n\t\t\tc:      \"#ffffff\",\n\t\t\texpect: \"#000000\",\n\t\t},\n\t\t\"red_to_dark\": {\n\t\t\t// L=0.628, C=0.258, h=29.2\n\t\t\tc:      \"#ff0000\",\n\t\t\texpect: \"#7e0000\",\n\t\t},\n\t\t\"blue_to_light\": {\n\t\t\t// L=0.452, C=0.313, h=264.1 -> L adjusted to 0.55 to preserve chroma\n\t\t\tc:      \"#0000ff\",\n\t\t\texpect: \"#1f5bff\",\n\t\t},\n\t\t\"green_to_dark\": {\n\t\t\t// L=0.866, C=0.295, h=142.5 -> L adjusted to 0.44 to preserve chroma\n\t\t\tc:      \"#00ff00\",\n\t\t\texpect: \"#006600\",\n\t\t},\n\t\t\"yellow_to_dark\": {\n\t\t\t// L=0.968, C=0.211, h=109.8 -> L adjusted to 0.49 to preserve chroma\n\t\t\tc:      \"#ffff00\",\n\t\t\texpect: \"#656501\",\n\t\t},\n\t\t\"cyan_to_dark\": {\n\t\t\t// L=0.905, C=0.155, h=194.8 -> L adjusted to 0.46 to preserve chroma\n\t\t\tc:      \"#00ffff\",\n\t\t\texpect: \"#016464\",\n\t\t},\n\t\t\"dark_gray_to_light\": {\n\t\t\tc:      \"#333333\",\n\t\t\texpect: \"#989898\",\n\t\t},\n\t\t\"light_gray_to_dark\": {\n\t\t\tc:      \"#cccccc\",\n\t\t\texpect: \"#0c0c0c\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tc := config.NewColor(u.c)\n\t\t\tinverted := c.InvertColor()\n\t\t\tassert.Equal(t, u.expect, string(inverted))\n\t\t})\n\t}\n}\n\nfunc TestInvertColorPreservesHue(t *testing.T) {\n\t// Verify that hue is preserved during inversion for chromatic colors.\n\t// Note: Hue preservation depends on the inverted color having sufficient chroma\n\t// and not being clamped by go-colorful's Clamped() method. Colors near the\n\t// gamut boundary may have hue shifts after clamping.\n\tuu := map[string]struct {\n\t\tc string\n\t\th float64 // expected hue\n\t}{\n\t\t\"red\": {\n\t\t\t// L=0.628, C=0.258, h=29.2 -> inverted to L=0.372 with good chroma\n\t\t\tc: \"#ff0000\",\n\t\t\th: 29.2,\n\t\t},\n\t\t\"blue\": {\n\t\t\t// L=0.452, C=0.313, h=264.1 -> inverted to L=0.548 with good chroma\n\t\t\tc: \"#0000ff\",\n\t\t\th: 264.1,\n\t\t},\n\t\t\"purple\": {\n\t\t\t// L=0.420, C=0.161, h=328.4 -> mid-lightness, stable hue\n\t\t\tc: \"#800080\",\n\t\t\th: 328.4,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\toriginal := config.NewColor(u.c)\n\t\t\tinverted := original.InvertColor()\n\n\t\t\t_, hOrig := getOkch(u.c)\n\t\t\tcInv, hInv := getOkch(string(inverted))\n\n\t\t\t// Only check hue if inverted color has meaningful chroma (C > 0.05)\n\t\t\t// Below this threshold, sRGB quantization causes hue instability\n\t\t\tif cInv > 0.05 {\n\t\t\t\tassert.True(t, huesEqual(hOrig, hInv, 1.0),\n\t\t\t\t\t\"hue should be preserved: original h=%.1f, inverted h=%.1f\", hOrig, hInv)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestInvertGrayRoundTrip(t *testing.T) {\n\t// Achromatic colors (grays) round-trip perfectly because they have no chroma\n\t// to lose during gamut-constrained scaling.\n\tcolors := []string{\n\t\t\"#000000\",\n\t\t\"#ffffff\",\n\t\t\"#808080\",\n\t\t\"#333333\",\n\t\t\"#cccccc\",\n\t\t\"#555555\",\n\t\t\"#636363\", // L=0.5 in Oklch\n\t}\n\n\tfor _, c := range colors {\n\t\tt.Run(c, func(t *testing.T) {\n\t\t\toriginal := config.NewColor(c)\n\t\t\tinverted := original.InvertColor()\n\t\t\treinverted := inverted.InvertColor()\n\n\t\t\tassert.Equal(t, original.String(), string(reinverted),\n\t\t\t\t\"double inversion should return to original for achromatic colors\")\n\t\t})\n\t}\n}\n\nfunc TestInvertColorSelfInverting(t *testing.T) {\n\t// Colors with L=0.5 in Oklch invert to themselves.\n\t// For achromatic grays, L=0.5 corresponds to approximately #636363 in sRGB.\n\tselfInverting := []string{\n\t\t\"#636363\",\n\t}\n\n\tfor _, c := range selfInverting {\n\t\tt.Run(c, func(t *testing.T) {\n\t\t\toriginal := config.NewColor(c)\n\t\t\tinverted := original.InvertColor()\n\n\t\t\tassert.Equal(t, original.String(), string(inverted),\n\t\t\t\t\"color with L=0.5 should invert to itself\")\n\t\t})\n\t}\n}\n\nfunc TestInvertColorOutOfGamut(t *testing.T) {\n\t// These highly saturated colors would produce out-of-gamut results if we\n\t// simply inverted L without adjustment. The chroma-preserving approach\n\t// finds an L closer to 0.5 where sufficient chroma is available.\n\t//\n\t// For colors with very high L (yellow, cyan), the ideal inverted L would\n\t// be very low where max chroma is tiny. Instead, L is adjusted toward 0.5\n\t// to preserve chromaPreserveFactor (0.5) of the original chroma.\n\tuu := map[string]struct {\n\t\tc      string\n\t\texpect string\n\t}{\n\t\t\"saturated_yellow\": {\n\t\t\t// L=0.968, C=0.211 -> L adjusted to 0.49 to preserve 50% chroma\n\t\t\tc:      \"#ffff00\",\n\t\t\texpect: \"#656501\",\n\t\t},\n\t\t\"saturated_cyan\": {\n\t\t\t// L=0.905, C=0.155 -> L adjusted to 0.46 to preserve 50% chroma\n\t\t\tc:      \"#00ffff\",\n\t\t\texpect: \"#016464\",\n\t\t},\n\t\t\"saturated_blue\": {\n\t\t\t// L=0.452, C=0.313 -> L adjusted to 0.55 to preserve chroma\n\t\t\tc:      \"#0000ff\",\n\t\t\texpect: \"#1f5bff\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\toriginal := config.NewColor(u.c)\n\t\t\tinverted := original.InvertColor()\n\n\t\t\t// Verify the inverted color matches expected\n\t\t\tinvertedStr := string(inverted)\n\t\t\tassert.Equal(t, u.expect, invertedStr)\n\n\t\t\t// Verify the inverted color is valid hex\n\t\t\tassert.Regexp(t, `^#[0-9a-f]{6}$`, invertedStr,\n\t\t\t\t\"inverted color should be valid hex\")\n\n\t\t\t// Verify it differs from original (these are not L=0.5 colors)\n\t\t\tassert.NotEqual(t, original.String(), invertedStr,\n\t\t\t\t\"saturated color should not invert to itself\")\n\n\t\t\t// Only check hue preservation for colors with meaningful inverted chroma (C > 0.05)\n\t\t\t// Below this threshold, sRGB quantization causes hue instability\n\t\t\tcInv, hInv := getOkch(invertedStr)\n\t\t\tif cInv > 0.05 {\n\t\t\t\t_, hOrig := getOkch(u.c)\n\t\t\t\tassert.True(t, huesEqual(hOrig, hInv, 1.0),\n\t\t\t\t\t\"hue should be preserved: original h=%.1f, inverted h=%.1f\", hOrig, hInv)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/config/config.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/config/json\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n\t\"gopkg.in/yaml.v3\"\n\t\"k8s.io/cli-runtime/pkg/genericclioptions\"\n)\n\n// Config tracks K9s configuration options.\ntype Config struct {\n\tK9s      *K9s `yaml:\"k9s\" json:\"k9s\"`\n\tconn     client.Connection\n\tsettings data.KubeSettings\n}\n\n// NewConfig creates a new default config.\nfunc NewConfig(ks data.KubeSettings) *Config {\n\treturn &Config{\n\t\tsettings: ks,\n\t\tK9s:      NewK9s(nil, ks),\n\t}\n}\n\n// IsReadOnly returns true if K9s is running in read-only mode.\nfunc (c *Config) IsReadOnly() bool {\n\treturn c.K9s.IsReadOnly()\n}\n\n// ActiveClusterName returns the corresponding cluster name.\nfunc (c *Config) ActiveClusterName(contextName string) (string, error) {\n\tct, err := c.settings.GetContext(contextName)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn ct.Cluster, nil\n}\n\n// ContextHotkeysPath returns a context specific hotkeys file spec.\nfunc (c *Config) ContextHotkeysPath() string {\n\tct, err := c.K9s.ActiveContext()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn AppContextHotkeysFile(ct.ClusterName, c.K9s.activeContextName)\n}\n\n// ContextAliasesPath returns a context specific aliases file spec.\nfunc (c *Config) ContextAliasesPath() string {\n\tct, err := c.K9s.ActiveContext()\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn AppContextAliasesFile(ct.GetClusterName(), c.K9s.activeContextName)\n}\n\n// ContextPluginsPath returns a context specific plugins file spec.\nfunc (c *Config) ContextPluginsPath() (string, error) {\n\tct, err := c.K9s.ActiveContext()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn AppContextPluginsFile(ct.GetClusterName(), c.K9s.activeContextName), nil\n}\n\nfunc setK8sTimeout(flags *genericclioptions.ConfigFlags, d time.Duration) {\n\tv := d.String()\n\tflags.Timeout = &v\n}\n\n// Refine the configuration based on cli args.\nfunc (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, cfg *client.Config) error {\n\tif flags == nil {\n\t\treturn nil\n\t}\n\n\tif !isStringSet(flags.Timeout) {\n\t\tif d, err := time.ParseDuration(c.K9s.APIServerTimeout); err == nil {\n\t\t\tsetK8sTimeout(flags, d)\n\t\t} else {\n\t\t\tsetK8sTimeout(flags, client.DefaultCallTimeoutDuration)\n\t\t}\n\t}\n\tif isStringSet(flags.Context) {\n\t\tif _, err := c.K9s.ActivateContext(*flags.Context); err != nil {\n\t\t\treturn fmt.Errorf(\"k8sflags. unable to activate context %q: %w\", *flags.Context, err)\n\t\t}\n\t} else {\n\t\tn, err := cfg.CurrentContextName()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to retrieve kubeconfig current context %q: %w\", n, err)\n\t\t}\n\t\t_, err = c.K9s.ActivateContext(n)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"unable to activate context %q: %w\", n, err)\n\t\t}\n\t}\n\tslog.Debug(\"Using active context\", slogs.Context, c.K9s.ActiveContextName())\n\n\tvar ns string\n\tswitch {\n\tcase k9sFlags != nil && IsBoolSet(k9sFlags.AllNamespaces):\n\t\tns = client.NamespaceAll\n\t\tc.ResetActiveView()\n\tcase isStringSet(flags.Namespace):\n\t\tns = *flags.Namespace\n\t\tc.ResetActiveView()\n\tdefault:\n\t\tnss, err := c.K9s.ActiveContextNamespace()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tns = nss\n\t}\n\tif ns == \"\" {\n\t\tns = client.DefaultNamespace\n\t}\n\tif err := c.SetActiveNamespace(ns); err != nil {\n\t\treturn err\n\t}\n\n\treturn data.EnsureDirPath(c.K9s.AppScreenDumpDir(), data.DefaultDirMod)\n}\n\n// Reset resets the context to the new current context/cluster.\nfunc (c *Config) Reset() {\n\tc.K9s.Reset()\n}\n\nfunc (c *Config) ActivateContext(n string) (*data.Context, error) {\n\tct, err := c.K9s.ActivateContext(n)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"set current context failed. %w\", err)\n\t}\n\n\treturn ct, nil\n}\n\n// CurrentContext fetch the configuration active context.\nfunc (c *Config) CurrentContext() (*data.Context, error) {\n\treturn c.K9s.ActiveContext()\n}\n\n// ActiveNamespace returns the active namespace in the current context.\n// If none found return the empty ns.\nfunc (c *Config) ActiveNamespace() string {\n\tns, err := c.K9s.ActiveContextNamespace()\n\tif err != nil {\n\t\tslog.Error(\"Unable to assert active namespace. Using default\", slogs.Error, err)\n\t\tns = client.DefaultNamespace\n\t}\n\n\treturn ns\n}\n\n// FavNamespaces returns fav namespaces in the current context.\nfunc (c *Config) FavNamespaces() []string {\n\tct, err := c.K9s.ActiveContext()\n\tif err != nil {\n\t\treturn nil\n\t}\n\tct.Validate(c.conn, c.K9s.getActiveContextName(), ct.ClusterName)\n\n\treturn ct.Namespace.Favorites\n}\n\n// SetActiveNamespace set the active namespace in the current context.\nfunc (c *Config) SetActiveNamespace(ns string) error {\n\tif ns == client.NotNamespaced {\n\t\tslog.Debug(\"No namespace given. skipping!\", slogs.Namespace, ns)\n\t\treturn nil\n\t}\n\tct, err := c.K9s.ActiveContext()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn ct.Namespace.SetActive(ns, c.settings)\n}\n\n// ActiveView returns the active view in the current context.\nfunc (c *Config) ActiveView() string {\n\tct, err := c.K9s.ActiveContext()\n\tif err != nil {\n\t\treturn data.DefaultView\n\t}\n\tv := ct.View.Active\n\tif c.K9s.manualCommand != nil && *c.K9s.manualCommand != \"\" {\n\t\tv = *c.K9s.manualCommand\n\t\t// We reset the manualCommand property because\n\t\t// the command-line switch should only be considered once,\n\t\t// on startup.\n\t\t*c.K9s.manualCommand = \"\"\n\t}\n\n\treturn v\n}\n\nfunc (c *Config) ResetActiveView() {\n\tif isStringSet(c.K9s.manualCommand) {\n\t\treturn\n\t}\n\tv := c.ActiveView()\n\tif v == \"\" {\n\t\treturn\n\t}\n\tp := cmd.NewInterpreter(v)\n\tif p.HasNS() {\n\t\tc.SetActiveView(p.Cmd())\n\t}\n}\n\n// SetActiveView sets current context active view.\nfunc (c *Config) SetActiveView(view string) {\n\tif ct, err := c.K9s.ActiveContext(); err == nil {\n\t\tct.View.Active = view\n\t}\n}\n\n// GetConnection return an api server connection.\nfunc (c *Config) GetConnection() client.Connection {\n\treturn c.conn\n}\n\n// SetConnection set an api server connection.\nfunc (c *Config) SetConnection(conn client.Connection) {\n\tc.conn = conn\n\tif conn != nil {\n\t\tc.K9s.resetConnection(conn)\n\t}\n}\n\nfunc (c *Config) ActiveContextName() string {\n\treturn c.K9s.activeContextName\n}\n\nfunc (c *Config) Merge(c1 *Config) {\n\tc.K9s.Merge(c1.K9s)\n}\n\n// Load loads K9s configuration from file.\nfunc (c *Config) Load(path string, force bool) error {\n\tif _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {\n\t\tif err := c.Save(force); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tbb, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar errs error\n\tif err := data.JSONValidator.Validate(json.K9sSchema, bb); err != nil {\n\t\terrs = errors.Join(errs, fmt.Errorf(\"k9s config file %q load failed:\\n%w\", path, err))\n\t}\n\n\tvar cfg Config\n\tif err := yaml.Unmarshal(bb, &cfg); err != nil {\n\t\terrs = errors.Join(errs, fmt.Errorf(\"main config.yaml load failed: %w\", err))\n\t}\n\tc.Merge(&cfg)\n\n\treturn errs\n}\n\n// Save configuration to disk.\nfunc (c *Config) Save(force bool) error {\n\tcontextName := c.K9s.ActiveContextName()\n\tclusterName, err := c.ActiveClusterName(contextName)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to locate associated cluster for context %q: %w\", contextName, err)\n\t}\n\tc.Validate(contextName, clusterName)\n\tif err := c.K9s.Save(contextName, clusterName, force); err != nil {\n\t\treturn err\n\t}\n\tif _, err := os.Stat(AppConfigFile); errors.Is(err, fs.ErrNotExist) {\n\t\treturn c.SaveFile(AppConfigFile)\n\t}\n\n\treturn nil\n}\n\n// SaveFile K9s configuration to disk.\nfunc (c *Config) SaveFile(path string) error {\n\tif err := data.EnsureDirPath(path, data.DefaultDirMod); err != nil {\n\t\treturn err\n\t}\n\n\tif err := data.SaveYAML(path, c); err != nil {\n\t\tslog.Error(\"Unable to save K9s config file\", slogs.Error, err)\n\t\treturn err\n\t}\n\n\tslog.Info(\"[CONFIG] Saving K9s config to disk\", slogs.Path, path)\n\treturn nil\n}\n\n// Validate the configuration.\nfunc (c *Config) Validate(contextName, clusterName string) {\n\tif c.K9s == nil {\n\t\tc.K9s = NewK9s(c.conn, c.settings)\n\t}\n\tc.K9s.Validate(c.conn, contextName, clusterName)\n}\n\n// Dump for debug...\nfunc (c *Config) Dump(msg string) {\n\tct, err := c.K9s.ActiveContext()\n\tif err == nil {\n\t\tbb, _ := yaml.Marshal(ct)\n\t\tfmt.Printf(\"Dump: %q\\n%s\\n\", msg, string(bb))\n\t} else {\n\t\tfmt.Println(\"BOOM!\", err)\n\t}\n}\n"
  },
  {
    "path": "internal/config/config_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config_test\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/adrg/xdg\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/config/mock\"\n\tm \"github.com/petergtz/pegomock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/cli-runtime/pkg/genericclioptions\"\n)\n\nfunc init() {\n\tslog.SetDefault(slog.New(slog.DiscardHandler))\n}\n\nfunc TestConfigSave(t *testing.T) {\n\tconfig.AppConfigFile = \"/tmp/k9s-test/k9s.yaml\"\n\tsd := \"/tmp/k9s-test/screen-dumps\"\n\tcl, ct := \"cl-1\", \"ct-1-1\"\n\t_ = os.RemoveAll((\"/tmp/k9s-test\"))\n\n\tuu := map[string]struct {\n\t\tct       string\n\t\tflags    *genericclioptions.ConfigFlags\n\t\tk9sFlags *config.Flags\n\t}{\n\t\t\"happy\": {\n\t\t\tct: \"ct-1-1\",\n\t\t\tflags: &genericclioptions.ConfigFlags{\n\t\t\t\tClusterName: &cl,\n\t\t\t\tContext:     &ct,\n\t\t\t},\n\t\t\tk9sFlags: &config.Flags{\n\t\t\t\tScreenDumpDir: &sd,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\txdg.Reload()\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tc := mock.NewMockConfig(t)\n\t\t\t_, err := c.K9s.ActivateContext(u.ct)\n\t\t\trequire.NoError(t, err)\n\t\t\tif u.flags != nil {\n\t\t\t\tc.K9s.Override(u.k9sFlags)\n\t\t\t\trequire.NoError(t, c.Refine(u.flags, u.k9sFlags, client.NewConfig(u.flags)))\n\t\t\t}\n\t\t\trequire.NoError(t, c.Save(true))\n\t\t\tbb, err := os.ReadFile(config.AppConfigFile)\n\t\t\trequire.NoError(t, err)\n\t\t\tee, err := os.ReadFile(\"testdata/configs/default.yaml\")\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, string(ee), string(bb))\n\t\t})\n\t}\n}\n\nfunc TestSetActiveView(t *testing.T) {\n\tvar (\n\t\tcfgFile = \"testdata/kubes/test.yaml\"\n\t\tview    = \"dp\"\n\t)\n\n\tuu := map[string]struct {\n\t\tct       string\n\t\tflags    *genericclioptions.ConfigFlags\n\t\tk9sFlags *config.Flags\n\t\tview     string\n\t\te        string\n\t}{\n\t\t\"empty\": {\n\t\t\tview: data.DefaultView,\n\t\t\te:    data.DefaultView,\n\t\t},\n\t\t\"not-exists\": {\n\t\t\tct:   \"fred\",\n\t\t\tview: data.DefaultView,\n\t\t\te:    data.DefaultView,\n\t\t},\n\t\t\"happy\": {\n\t\t\tct:   \"ct-1-1\",\n\t\t\tview: \"xray\",\n\t\t\te:    \"xray\",\n\t\t},\n\t\t\"cli-override\": {\n\t\t\tflags: &genericclioptions.ConfigFlags{\n\t\t\t\tKubeConfig: &cfgFile,\n\t\t\t},\n\t\t\tk9sFlags: &config.Flags{\n\t\t\t\tCommand: &view,\n\t\t\t},\n\t\t\tct:   \"ct-1-1\",\n\t\t\tview: \"xray\",\n\t\t\te:    \"dp\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tc := mock.NewMockConfig(t)\n\t\t\t_, _ = c.K9s.ActivateContext(u.ct)\n\t\t\tif u.flags != nil {\n\t\t\t\trequire.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags)))\n\t\t\t\tc.K9s.Override(u.k9sFlags)\n\t\t\t}\n\t\t\tc.SetActiveView(u.view)\n\t\t\tassert.Equal(t, u.e, c.ActiveView())\n\t\t})\n\t}\n}\n\nfunc TestActiveContextName(t *testing.T) {\n\tvar (\n\t\tcfgFile = \"testdata/kubes/test.yaml\"\n\t\tct2     = \"ct-1-2\"\n\t)\n\n\tuu := map[string]struct {\n\t\tflags    *genericclioptions.ConfigFlags\n\t\tk9sFlags *config.Flags\n\t\tct       string\n\t\te        string\n\t}{\n\t\t\"empty\": {},\n\t\t\"happy\": {\n\t\t\tct: \"ct-1-1\",\n\t\t\te:  \"ct-1-1\",\n\t\t},\n\t\t\"cli-override\": {\n\t\t\tflags: &genericclioptions.ConfigFlags{\n\t\t\t\tKubeConfig: &cfgFile,\n\t\t\t\tContext:    &ct2,\n\t\t\t},\n\t\t\tk9sFlags: &config.Flags{},\n\t\t\tct:       \"ct-1-1\",\n\t\t\te:        \"ct-1-2\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tc := mock.NewMockConfig(t)\n\t\t\t_, _ = c.K9s.ActivateContext(u.ct)\n\t\t\tif u.flags != nil {\n\t\t\t\trequire.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags)))\n\t\t\t\tc.K9s.Override(u.k9sFlags)\n\t\t\t}\n\t\t\tassert.Equal(t, u.e, c.ActiveContextName())\n\t\t})\n\t}\n}\n\nfunc TestActiveView(t *testing.T) {\n\tvar (\n\t\tcfgFile = \"testdata/kubes/test.yaml\"\n\t\tview    = \"dp\"\n\t)\n\n\tuu := map[string]struct {\n\t\tct       string\n\t\tflags    *genericclioptions.ConfigFlags\n\t\tk9sFlags *config.Flags\n\t\te        string\n\t}{\n\t\t\"empty\": {\n\t\t\te: data.DefaultView,\n\t\t},\n\n\t\t\"not-exists\": {\n\t\t\tct: \"fred\",\n\t\t\te:  data.DefaultView,\n\t\t},\n\n\t\t\"happy\": {\n\t\t\tct: \"ct-1-1\",\n\t\t\te:  data.DefaultView,\n\t\t},\n\n\t\t\"happy1\": {\n\t\t\tct: \"ct-1-2\",\n\t\t\te:  data.DefaultView,\n\t\t},\n\n\t\t\"cli-override\": {\n\t\t\tflags: &genericclioptions.ConfigFlags{\n\t\t\t\tKubeConfig: &cfgFile,\n\t\t\t},\n\t\t\tk9sFlags: &config.Flags{\n\t\t\t\tCommand: &view,\n\t\t\t},\n\t\t\te: \"dp\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tc := mock.NewMockConfig(t)\n\t\t\t_, _ = c.K9s.ActivateContext(u.ct)\n\t\t\tif u.flags != nil {\n\t\t\t\trequire.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags)))\n\t\t\t\tc.K9s.Override(u.k9sFlags)\n\t\t\t}\n\t\t\tassert.Equal(t, u.e, c.ActiveView())\n\t\t})\n\t}\n}\n\nfunc TestFavNamespaces(t *testing.T) {\n\tuu := map[string]struct {\n\t\tct string\n\t\te  []string\n\t}{\n\t\t\"empty\": {},\n\t\t\"not-exists\": {\n\t\t\tct: \"fred\",\n\t\t},\n\t\t\"happy\": {\n\t\t\tct: \"ct-1-1\",\n\t\t\te:  []string{client.DefaultNamespace},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tc := mock.NewMockConfig(t)\n\t\t\t_, _ = c.K9s.ActivateContext(u.ct)\n\t\t\tassert.Equal(t, u.e, c.FavNamespaces())\n\t\t})\n\t}\n}\n\nfunc TestContextAliasesPath(t *testing.T) {\n\tuu := map[string]struct {\n\t\tct string\n\t\te  string\n\t}{\n\t\t\"empty\": {},\n\t\t\"not-exists\": {\n\t\t\tct: \"fred\",\n\t\t},\n\t\t\"happy\": {\n\t\t\tct: \"ct-1-1\",\n\t\t\te:  \"/tmp/test/cl-1/ct-1-1/aliases.yaml\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tc := mock.NewMockConfig(t)\n\t\t\t_, _ = c.K9s.ActivateContext(u.ct)\n\t\t\tassert.Equal(t, u.e, c.ContextAliasesPath())\n\t\t})\n\t}\n}\n\nfunc TestContextPluginsPath(t *testing.T) {\n\tuu := map[string]struct {\n\t\tct, e string\n\t\terr   error\n\t}{\n\t\t\"empty\": {\n\t\t\terr: errors.New(`no context found for: \"\"`),\n\t\t},\n\t\t\"happy\": {\n\t\t\tct: \"ct-1-1\",\n\t\t\te:  \"/tmp/test/cl-1/ct-1-1/plugins.yaml\",\n\t\t},\n\t\t\"not-exists\": {\n\t\t\tct:  \"fred\",\n\t\t\terr: errors.New(`no context found for: \"fred\"`),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tc := mock.NewMockConfig(t)\n\t\t\t_, _ = c.K9s.ActivateContext(u.ct)\n\t\t\ts, err := c.ContextPluginsPath()\n\t\t\tif err != nil {\n\t\t\t\tassert.Equal(t, u.err, err)\n\t\t\t}\n\t\t\tassert.Equal(t, u.e, s)\n\t\t})\n\t}\n}\n\nfunc TestConfigLoader(t *testing.T) {\n\tuu := map[string]struct {\n\t\tf   string\n\t\terr string\n\t}{\n\t\t\"happy\": {\n\t\t\tf: \"testdata/configs/k9s.yaml\",\n\t\t},\n\t\t\"toast\": {\n\t\t\tf: \"testdata/configs/k9s_toast.yaml\",\n\t\t\terr: `k9s config file \"testdata/configs/k9s_toast.yaml\" load failed:\nAdditional property disablePodCounts is not allowed\nAdditional property shellPods is not allowed\nInvalid type. Expected: boolean, given: string`,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tcfg := config.NewConfig(nil)\n\t\t\tif err := cfg.Load(u.f, true); err != nil {\n\t\t\t\tassert.Equal(t, u.err, err.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestConfigActivateContext(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcl, ct string\n\t\terr    string\n\t}{\n\t\t\"happy\": {\n\t\t\tct: \"ct-1-2\",\n\t\t\tcl: \"cl-1\",\n\t\t},\n\t\t\"toast\": {\n\t\t\tct:  \"fred\",\n\t\t\tcl:  \"cl-1\",\n\t\t\terr: `set current context failed. no context found for: \"fred\"`,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tcfg := mock.NewMockConfig(t)\n\t\t\tct, err := cfg.ActivateContext(u.ct)\n\t\t\tif err != nil {\n\t\t\t\tassert.Equal(t, u.err, err.Error())\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, u.cl, ct.ClusterName)\n\t\t})\n\t}\n}\n\nfunc TestConfigCurrentContext(t *testing.T) {\n\tvar (\n\t\tcfgFile = \"testdata/kubes/test.yaml\"\n\t\tct2     = \"ct-1-2\"\n\t)\n\n\tuu := map[string]struct {\n\t\tflags     *genericclioptions.ConfigFlags\n\t\terr       error\n\t\tcontext   string\n\t\tcluster   string\n\t\tnamespace string\n\t}{\n\t\t\"override-context\": {\n\t\t\tflags: &genericclioptions.ConfigFlags{\n\t\t\t\tKubeConfig: &cfgFile,\n\t\t\t\tContext:    &ct2,\n\t\t\t},\n\t\t\tcluster:   \"cl-1\",\n\t\t\tcontext:   \"ct-1-2\",\n\t\t\tnamespace: \"ns-2\",\n\t\t},\n\t\t\"use-current-context\": {\n\t\t\tflags: &genericclioptions.ConfigFlags{\n\t\t\t\tKubeConfig: &cfgFile,\n\t\t\t},\n\t\t\tcluster:   \"cl-1\",\n\t\t\tcontext:   \"ct-1-1\",\n\t\t\tnamespace: client.DefaultNamespace,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tcfg := mock.NewMockConfig(t)\n\n\t\t\terr := cfg.Refine(u.flags, nil, client.NewConfig(u.flags))\n\t\t\trequire.NoError(t, err)\n\t\t\tct, err := cfg.CurrentContext()\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, u.cluster, ct.ClusterName)\n\t\t\tassert.Equal(t, u.namespace, ct.Namespace.Active)\n\t\t})\n\t}\n}\n\nfunc TestConfigRefine(t *testing.T) {\n\tvar (\n\t\tcfgFile       = \"testdata/kubes/test.yaml\"\n\t\tcl1           = \"cl-1\"\n\t\tct2           = \"ct-1-2\"\n\t\tns1, ns2, nsx = \"ns-1\", \"ns-2\", \"ns-x\"\n\t\ttrueVal       = true\n\t)\n\n\tuu := map[string]struct {\n\t\tflags     *genericclioptions.ConfigFlags\n\t\tk9sFlags  *config.Flags\n\t\terr       string\n\t\tcontext   string\n\t\tcluster   string\n\t\tnamespace string\n\t}{\n\t\t\"no-override\": {\n\t\t\tnamespace: \"default\",\n\t\t},\n\t\t\"override-cluster\": {\n\t\t\tflags: &genericclioptions.ConfigFlags{\n\t\t\t\tKubeConfig:  &cfgFile,\n\t\t\t\tClusterName: &cl1,\n\t\t\t},\n\t\t\tcluster:   \"cl-1\",\n\t\t\tcontext:   \"ct-1-1\",\n\t\t\tnamespace: client.DefaultNamespace,\n\t\t},\n\t\t\"override-cluster-context\": {\n\t\t\tflags: &genericclioptions.ConfigFlags{\n\t\t\t\tKubeConfig:  &cfgFile,\n\t\t\t\tClusterName: &cl1,\n\t\t\t\tContext:     &ct2,\n\t\t\t},\n\t\t\tcluster:   \"cl-1\",\n\t\t\tcontext:   \"ct-1-2\",\n\t\t\tnamespace: \"ns-2\",\n\t\t},\n\t\t\"override-bad-cluster\": {\n\t\t\tflags: &genericclioptions.ConfigFlags{\n\t\t\t\tKubeConfig:  &cfgFile,\n\t\t\t\tClusterName: &ns1,\n\t\t\t},\n\t\t\tcluster:   \"cl-1\",\n\t\t\tcontext:   \"ct-1-1\",\n\t\t\tnamespace: client.DefaultNamespace,\n\t\t},\n\t\t\"override-ns\": {\n\t\t\tflags: &genericclioptions.ConfigFlags{\n\t\t\t\tKubeConfig: &cfgFile,\n\t\t\t\tNamespace:  &ns2,\n\t\t\t},\n\t\t\tcluster:   \"cl-1\",\n\t\t\tcontext:   \"ct-1-1\",\n\t\t\tnamespace: \"ns-2\",\n\t\t},\n\t\t\"all-ns\": {\n\t\t\tflags: &genericclioptions.ConfigFlags{\n\t\t\t\tKubeConfig: &cfgFile,\n\t\t\t\tNamespace:  &ns2,\n\t\t\t},\n\t\t\tk9sFlags: &config.Flags{\n\t\t\t\tAllNamespaces: &trueVal,\n\t\t\t},\n\t\t\tcluster:   \"cl-1\",\n\t\t\tcontext:   \"ct-1-1\",\n\t\t\tnamespace: client.NamespaceAll,\n\t\t},\n\n\t\t\"override-bad-ns\": {\n\t\t\tflags: &genericclioptions.ConfigFlags{\n\t\t\t\tKubeConfig: &cfgFile,\n\t\t\t\tNamespace:  &nsx,\n\t\t\t},\n\t\t\tcluster:   \"cl-1\",\n\t\t\tcontext:   \"ct-1-1\",\n\t\t\tnamespace: \"ns-x\",\n\t\t},\n\t\t\"override-context\": {\n\t\t\tflags: &genericclioptions.ConfigFlags{\n\t\t\t\tKubeConfig: &cfgFile,\n\t\t\t\tContext:    &ct2,\n\t\t\t},\n\t\t\tcluster:   \"cl-1\",\n\t\t\tcontext:   \"ct-1-2\",\n\t\t\tnamespace: \"ns-2\",\n\t\t},\n\t\t\"override-bad-context\": {\n\t\t\tflags: &genericclioptions.ConfigFlags{\n\t\t\t\tKubeConfig: &cfgFile,\n\t\t\t\tContext:    &ns1,\n\t\t\t},\n\t\t\terr: `k8sflags. unable to activate context \"ns-1\": no context found for: \"ns-1\"`,\n\t\t},\n\t\t\"use-current-context\": {\n\t\t\tflags: &genericclioptions.ConfigFlags{\n\t\t\t\tKubeConfig: &cfgFile,\n\t\t\t},\n\t\t\tcluster:   \"cl-1\",\n\t\t\tcontext:   \"ct-1-1\",\n\t\t\tnamespace: client.DefaultNamespace,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tcfg := mock.NewMockConfig(t)\n\n\t\t\terr := cfg.Refine(u.flags, u.k9sFlags, client.NewConfig(u.flags))\n\t\t\tif err != nil {\n\t\t\t\tassert.Equal(t, u.err, err.Error())\n\t\t\t} else {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tassert.Equal(t, u.context, cfg.K9s.ActiveContextName())\n\t\t\t\tassert.Equal(t, u.namespace, cfg.ActiveNamespace())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestConfigValidate(t *testing.T) {\n\tcfg := mock.NewMockConfig(t)\n\tcfg.SetConnection(mock.NewMockConnection())\n\n\trequire.NoError(t, cfg.Load(\"testdata/configs/k9s.yaml\", true))\n\tcfg.Validate(\"ct-1-1\", \"cl-1\")\n}\n\nfunc TestConfigLoad(t *testing.T) {\n\tcfg := mock.NewMockConfig(t)\n\n\trequire.NoError(t, cfg.Load(\"testdata/configs/k9s.yaml\", true))\n\tassert.InDelta(t, 2.0, cfg.K9s.RefreshRate, 0.001)\n\tassert.Equal(t, int64(200), cfg.K9s.Logger.TailCount)\n\tassert.Equal(t, 2000, cfg.K9s.Logger.BufferSize)\n}\n\nfunc TestConfigLoadCrap(t *testing.T) {\n\tcfg := mock.NewMockConfig(t)\n\n\tassert.Error(t, cfg.Load(\"testdata/configs/k9s_not_there.yaml\", true))\n}\n\nfunc TestConfigSaveFile(t *testing.T) {\n\tcfg := mock.NewMockConfig(t)\n\n\trequire.NoError(t, cfg.Load(\"testdata/configs/k9s.yaml\", true))\n\n\tcfg.K9s.RefreshRate = 100\n\tcfg.K9s.GPUVendors = map[string]string{\n\t\t\"bozo\": \"bozo/gpu.com\",\n\t}\n\tcfg.K9s.APIServerTimeout = \"30s\"\n\tcfg.K9s.ReadOnly = true\n\tcfg.K9s.Logger.TailCount = 500\n\tcfg.K9s.Logger.BufferSize = 800\n\tcfg.K9s.UI.UseFullGVRTitle = true\n\tcfg.Validate(\"ct-1-1\", \"cl-1\")\n\n\tpath := filepath.Join(os.TempDir(), \"k9s.yaml\")\n\trequire.NoError(t, cfg.SaveFile(path))\n\traw, err := os.ReadFile(path)\n\trequire.NoError(t, err)\n\tee, err := os.ReadFile(\"testdata/configs/expected.yaml\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, string(ee), string(raw))\n}\n\nfunc TestConfigReset(t *testing.T) {\n\tcfg := mock.NewMockConfig(t)\n\trequire.NoError(t, cfg.Load(\"testdata/configs/k9s.yaml\", true))\n\tcfg.Reset()\n\tcfg.Validate(\"ct-1-1\", \"cl-1\")\n\n\tpath := filepath.Join(os.TempDir(), \"k9s.yaml\")\n\trequire.NoError(t, cfg.SaveFile(path))\n\n\tbb, err := os.ReadFile(path)\n\trequire.NoError(t, err)\n\tee, err := os.ReadFile(\"testdata/configs/k9s.yaml\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, string(ee), string(bb))\n}\n\n// Helpers...\n\nfunc TestSetup(t *testing.T) {\n\tm.RegisterMockTestingT(t)\n\tm.RegisterMockFailHandler(func(m string, i ...int) {\n\t\tfmt.Println(\"Boom!\", m, i)\n\t})\n}\n"
  },
  {
    "path": "internal/config/data/config.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage data\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"sync\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"gopkg.in/yaml.v3\"\n\t\"k8s.io/client-go/tools/clientcmd/api\"\n)\n\n// Config tracks a context configuration.\ntype Config struct {\n\tContext *Context `yaml:\"k9s\"`\n\tmx      sync.RWMutex\n}\n\n// NewConfig returns a new config.\nfunc NewConfig(ct *api.Context) *Config {\n\treturn &Config{\n\t\tContext: NewContextFromConfig(ct),\n\t}\n}\n\n// Merge merges configs and updates receiver.\nfunc (c *Config) Merge(c1 *Config) {\n\tif c1 == nil {\n\t\treturn\n\t}\n\tif c.Context != nil && c1.Context != nil {\n\t\tc.Context.merge(c1.Context)\n\t}\n}\n\n// Validate ensures config is in norms.\nfunc (c *Config) Validate(conn client.Connection, contextName, clusterName string) {\n\tc.mx.Lock()\n\tdefer c.mx.Unlock()\n\n\tif c.Context == nil {\n\t\tc.Context = NewContext()\n\t}\n\tc.Context.Validate(conn, contextName, clusterName)\n}\n\n// Dump used for debugging.\nfunc (c *Config) Dump(w io.Writer) {\n\tbb, _ := yaml.Marshal(&c)\n\n\t_, _ = fmt.Fprintf(w, \"%s\\n\", string(bb))\n}\n"
  },
  {
    "path": "internal/config/data/context.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage data\n\nimport (\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"k8s.io/client-go/tools/clientcmd/api\"\n)\n\n// Context tracks K9s context configuration.\ntype Context struct {\n\tClusterName  string       `yaml:\"cluster,omitempty\"`\n\tReadOnly     *bool        `yaml:\"readOnly,omitempty\"`\n\tSkin         string       `yaml:\"skin,omitempty\"`\n\tNamespace    *Namespace   `yaml:\"namespace\"`\n\tView         *View        `yaml:\"view\"`\n\tFeatureGates FeatureGates `yaml:\"featureGates\"`\n\tProxy        *Proxy       `yaml:\"proxy\"`\n\tmx           sync.RWMutex\n}\n\n// NewContext creates a new cluster configuration.\nfunc NewContext() *Context {\n\treturn &Context{\n\t\tNamespace:    NewNamespace(),\n\t\tView:         NewView(),\n\t\tFeatureGates: NewFeatureGates(),\n\t}\n}\n\n// NewContextFromConfig returns a config based on a kubecontext.\nfunc NewContextFromConfig(cfg *api.Context) *Context {\n\tct := NewContext()\n\tct.Namespace, ct.ClusterName = NewActiveNamespace(cfg.Namespace), cfg.Cluster\n\n\treturn ct\n}\n\n// NewContextFromKubeConfig returns a new instance based on kubesettings or an error.\nfunc NewContextFromKubeConfig(ks KubeSettings) (*Context, error) {\n\tct, err := ks.CurrentContext()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn NewContextFromConfig(ct), nil\n}\n\nfunc (c *Context) merge(old *Context) {\n\tif old == nil || old.Namespace == nil {\n\t\treturn\n\t}\n\tif c.Namespace == nil {\n\t\tc.Namespace = NewNamespace()\n\t}\n\tc.Namespace.merge(old.Namespace)\n}\n\nfunc (c *Context) GetClusterName() string {\n\tc.mx.RLock()\n\tdefer c.mx.RUnlock()\n\n\treturn c.ClusterName\n}\n\n// Validate ensures a context config is tip top.\nfunc (c *Context) Validate(conn client.Connection, _, clusterName string) {\n\tc.mx.Lock()\n\tdefer c.mx.Unlock()\n\n\tc.ClusterName = clusterName\n\tif b := os.Getenv(envFGNodeShell); b != \"\" {\n\t\tc.FeatureGates.NodeShell = defaultFGNodeShell()\n\t}\n\n\tif c.Namespace == nil {\n\t\tc.Namespace = NewNamespace()\n\t}\n\tc.Namespace.Validate(conn)\n\n\tif c.View == nil {\n\t\tc.View = NewView()\n\t}\n\tc.View.Validate()\n}\n"
  },
  {
    "path": "internal/config/data/context_int_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage data\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_contextMerge(t *testing.T) {\n\tuu := map[string]struct {\n\t\tc1, c2, e *Context\n\t}{\n\t\t\"empty\": {},\n\t\t\"nil\": {\n\t\t\tc1: &Context{\n\t\t\t\tNamespace: &Namespace{\n\t\t\t\t\tActive:    \"ns1\",\n\t\t\t\t\tFavorites: []string{\"ns1\", \"ns2\", \"ns3\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: &Context{\n\t\t\t\tNamespace: &Namespace{\n\t\t\t\t\tActive:    \"ns1\",\n\t\t\t\t\tFavorites: []string{\"ns1\", \"ns2\", \"ns3\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"deltas\": {\n\t\t\tc1: &Context{\n\t\t\t\tNamespace: &Namespace{\n\t\t\t\t\tActive:    \"ns1\",\n\t\t\t\t\tFavorites: []string{\"ns1\", \"ns2\", \"ns3\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tc2: &Context{\n\t\t\t\tNamespace: &Namespace{\n\t\t\t\t\tActive:    \"ns10\",\n\t\t\t\t\tFavorites: []string{\"ns10\", \"ns11\", \"ns12\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: &Context{\n\t\t\t\tNamespace: &Namespace{\n\t\t\t\t\tActive:    \"ns1\",\n\t\t\t\t\tFavorites: []string{\"ns1\", \"ns2\", \"ns3\", \"ns10\", \"ns11\", \"ns12\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"deltas-locked\": {\n\t\t\tc1: &Context{\n\t\t\t\tNamespace: &Namespace{\n\t\t\t\t\tActive:        \"ns1\",\n\t\t\t\t\tLockFavorites: true,\n\t\t\t\t\tFavorites:     []string{\"ns1\", \"ns2\", \"ns3\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tc2: &Context{\n\t\t\t\tNamespace: &Namespace{\n\t\t\t\t\tActive:    \"ns10\",\n\t\t\t\t\tFavorites: []string{\"ns10\", \"ns11\", \"ns12\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: &Context{\n\t\t\t\tNamespace: &Namespace{\n\t\t\t\t\tActive:        \"ns1\",\n\t\t\t\t\tLockFavorites: true,\n\t\t\t\t\tFavorites:     []string{\"ns1\", \"ns2\", \"ns3\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"no-namespace\": {\n\t\t\tc1: NewContext(),\n\t\t\tc2: &Context{},\n\t\t\te:  NewContext(),\n\t\t},\n\t\t\"too-many-favs\": {\n\t\t\tc1: &Context{\n\t\t\t\tNamespace: &Namespace{\n\t\t\t\t\tActive:    \"ns1\",\n\t\t\t\t\tFavorites: []string{\"ns1\", \"ns2\", \"ns3\", \"ns4\", \"ns5\", \"ns6\", \"ns7\", \"ns8\", \"ns9\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tc2: &Context{\n\t\t\t\tNamespace: &Namespace{\n\t\t\t\t\tActive:    \"ns10\",\n\t\t\t\t\tFavorites: []string{\"ns10\", \"ns11\", \"ns12\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: &Context{\n\t\t\t\tNamespace: &Namespace{\n\t\t\t\t\tActive:    \"ns1\",\n\t\t\t\t\tFavorites: []string{\"ns1\", \"ns2\", \"ns3\", \"ns4\", \"ns5\", \"ns6\", \"ns7\", \"ns8\", \"ns9\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tu.c1.merge(u.c2)\n\t\t\tassert.Equal(t, u.e, u.c1)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/config/data/context_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage data_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/config/mock\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestClusterValidate(t *testing.T) {\n\tc := data.NewContext()\n\tc.Validate(mock.NewMockConnection(), \"ct-1\", \"cl-1\")\n\n\tassert.Equal(t, data.DefaultView, c.View.Active)\n\tassert.Equal(t, \"default\", c.Namespace.Active)\n\tassert.Len(t, c.Namespace.Favorites, 1)\n\tassert.Equal(t, []string{\"default\"}, c.Namespace.Favorites)\n}\n\nfunc TestClusterValidateEmpty(t *testing.T) {\n\tc := data.NewContext()\n\tc.Validate(mock.NewMockConnection(), \"ct-1\", \"cl-1\")\n\n\tassert.Equal(t, data.DefaultView, c.View.Active)\n\tassert.Equal(t, \"default\", c.Namespace.Active)\n\tassert.Len(t, c.Namespace.Favorites, 1)\n\tassert.Equal(t, []string{\"default\"}, c.Namespace.Favorites)\n}\n"
  },
  {
    "path": "internal/config/data/dir.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage data\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\n\t\"github.com/derailed/k9s/internal/config/json\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"gopkg.in/yaml.v3\"\n\t\"k8s.io/client-go/tools/clientcmd/api\"\n)\n\n// Dir tracks context configurations.\ntype Dir struct {\n\troot string\n\tmx   sync.Mutex\n}\n\n// NewDir returns a new instance.\nfunc NewDir(root string) *Dir {\n\treturn &Dir{\n\t\troot: root,\n\t}\n}\n\n// Load loads context configuration.\nfunc (d *Dir) Load(contextName string, ct *api.Context) (*Config, error) {\n\tif ct == nil {\n\t\treturn nil, errors.New(\"api.Context must not be nil\")\n\t}\n\n\tpath := filepath.Join(d.root, SanitizeContextSubpath(ct.Cluster, contextName), MainConfigFile)\n\tslog.Debug(\"[CONFIG] Loading context config from disk\", slogs.Path, path, slogs.Cluster, ct.Cluster, slogs.Context, contextName)\n\tf, err := os.Stat(path)\n\tif errors.Is(err, fs.ErrPermission) {\n\t\treturn nil, err\n\t}\n\tif errors.Is(err, fs.ErrNotExist) || (f != nil && f.Size() == 0) {\n\t\tslog.Debug(\"Context config not found! Generating..\", slogs.Path, path)\n\t\treturn d.genConfig(path, ct)\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn d.loadConfig(path)\n}\n\nfunc (d *Dir) genConfig(path string, ct *api.Context) (*Config, error) {\n\tcfg := NewConfig(ct)\n\tif err := d.Save(path, cfg); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn cfg, nil\n}\n\nfunc (d *Dir) Save(path string, c *Config) error {\n\tif cfg, err := d.loadConfig(path); err == nil {\n\t\tc.Merge(cfg)\n\t}\n\n\td.mx.Lock()\n\tdefer d.mx.Unlock()\n\n\tif err := EnsureDirPath(path, DefaultDirMod); err != nil {\n\t\treturn err\n\t}\n\n\treturn SaveYAML(path, c)\n}\n\nfunc (d *Dir) loadConfig(path string) (*Config, error) {\n\td.mx.Lock()\n\tdefer d.mx.Unlock()\n\n\tbb, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif err := JSONValidator.Validate(json.ContextSchema, bb); err != nil {\n\t\tslog.Warn(\"Validation failed. Please update your config and restart!\",\n\t\t\tslogs.Path, path,\n\t\t\tslogs.Error, err,\n\t\t)\n\t}\n\n\tvar cfg Config\n\tif err := yaml.Unmarshal(bb, &cfg); err != nil {\n\t\treturn nil, fmt.Errorf(\"context-config yaml load failed: %w\\n%s\", err, string(bb))\n\t}\n\n\treturn &cfg, nil\n}\n"
  },
  {
    "path": "internal/config/data/dir_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage data_test\n\nimport (\n\t\"log/slog\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/config/mock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"gopkg.in/yaml.v3\"\n\t\"k8s.io/cli-runtime/pkg/genericclioptions\"\n)\n\nfunc init() {\n\tslog.SetDefault(slog.New(slog.DiscardHandler))\n}\n\nfunc TestDirLoad(t *testing.T) {\n\tuu := map[string]struct {\n\t\tdir   string\n\t\tflags *genericclioptions.ConfigFlags\n\t\terr   error\n\t\tcfg   *data.Config\n\t}{\n\t\t\"happy-cl-1-ct-1\": {\n\t\t\tdir:   \"testdata/data/k9s\",\n\t\t\tflags: makeFlags(\"cl-1\", \"ct-1-1\"),\n\t\t\tcfg:   mustLoadConfig(\"testdata/configs/ct-1-1.yaml\"),\n\t\t},\n\n\t\t\"happy-cl-1-ct2\": {\n\t\t\tdir:   \"testdata/data/k9s\",\n\t\t\tflags: makeFlags(\"cl-1\", \"ct-1-2\"),\n\t\t\tcfg:   mustLoadConfig(\"testdata/configs/ct-1-2.yaml\"),\n\t\t},\n\n\t\t\"happy-cl-2\": {\n\t\t\tdir:   \"testdata/data/k9s\",\n\t\t\tflags: makeFlags(\"cl-2\", \"ct-2-1\"),\n\t\t\tcfg:   mustLoadConfig(\"testdata/configs/ct-2-1.yaml\"),\n\t\t},\n\n\t\t\"toast\": {\n\t\t\tdir:   \"/tmp/data/k9s\",\n\t\t\tflags: makeFlags(\"cl-test\", \"ct-test-1\"),\n\t\t\tcfg:   mustLoadConfig(\"testdata/configs/def_ct.yaml\"),\n\t\t},\n\n\t\t\"non-sanitized-path\": {\n\t\t\tdir:   \"/tmp/data/k9s\",\n\t\t\tflags: makeFlags(\"arn:aws:eks:eu-central-1:xxx:cluster/fred-blee\", \"fred-blee\"),\n\t\t\tcfg:   mustLoadConfig(\"testdata/configs/aws_ct.yaml\"),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.NotNil(t, u.cfg, \"test config must not be nil\")\n\t\t\tif u.cfg == nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tks := mock.NewMockKubeSettings(u.flags)\n\t\t\tif strings.Index(u.dir, \"/tmp\") == 0 {\n\t\t\t\trequire.NoError(t, mock.EnsureDir(u.dir))\n\t\t\t}\n\n\t\t\td := data.NewDir(u.dir)\n\t\t\tct, err := ks.CurrentContext()\n\t\t\trequire.NoError(t, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tcfg, err := d.Load(*u.flags.Context, ct)\n\t\t\tassert.Equal(t, u.err, err)\n\t\t\tif u.err == nil {\n\t\t\t\tassert.Equal(t, u.cfg, cfg)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Helpers...\n\nfunc makeFlags(cl, ct string) *genericclioptions.ConfigFlags {\n\treturn &genericclioptions.ConfigFlags{\n\t\tClusterName: &cl,\n\t\tContext:     &ct,\n\t}\n}\n\nfunc mustLoadConfig(cfg string) *data.Config {\n\tbb, err := os.ReadFile(cfg)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tvar ct data.Config\n\tif err = yaml.Unmarshal(bb, &ct); err != nil {\n\t\treturn nil\n\t}\n\n\treturn &ct\n}\n"
  },
  {
    "path": "internal/config/data/feature_gate.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage data\n\n// FeatureGates represents K9s opt-in features.\ntype FeatureGates struct {\n\tNodeShell bool `yaml:\"nodeShell\"`\n}\n\n// NewFeatureGates returns a new feature gate.\nfunc NewFeatureGates() FeatureGates {\n\treturn FeatureGates{}\n}\n"
  },
  {
    "path": "internal/config/data/helpers.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage data\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\nconst envFGNodeShell = \"K9S_FEATURE_GATE_NODE_SHELL\"\n\nvar invalidPathCharsRX = regexp.MustCompile(`[:/]+`)\n\n// SanitizeContextSubpath ensure cluster/context produces a valid path.\nfunc SanitizeContextSubpath(cluster, context string) string {\n\treturn filepath.Join(SanitizeFileName(cluster), SanitizeFileName(context))\n}\n\n// SanitizeFileName ensure file spec is valid.\nfunc SanitizeFileName(name string) string {\n\treturn invalidPathCharsRX.ReplaceAllString(name, \"-\")\n}\n\nfunc defaultFGNodeShell() bool {\n\tif a := os.Getenv(envFGNodeShell); a != \"\" {\n\t\treturn a == \"true\"\n\t}\n\n\treturn false\n}\n\n// EnsureDirPath ensures a directory exist from the given path.\nfunc EnsureDirPath(path string, mod os.FileMode) error {\n\treturn EnsureFullPath(filepath.Dir(path), mod)\n}\n\n// EnsureFullPath ensures a directory exist from the given path.\nfunc EnsureFullPath(path string, mod os.FileMode) error {\n\tif _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {\n\t\tif e := os.MkdirAll(path, mod); e != nil {\n\t\t\treturn e\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// WriteYAML writes a yaml file to bytes.\nfunc WriteYAML(content any) ([]byte, error) {\n\tbuff := bytes.NewBuffer(nil)\n\tec := yaml.NewEncoder(buff)\n\tec.SetIndent(2)\n\n\tif err := ec.Encode(content); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn buff.Bytes(), nil\n}\n\n// SaveYAML writes a yaml file to disk.\nfunc SaveYAML(path string, content any) error {\n\tf, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR|os.O_TRUNC, DefaultFileMod)\n\tif err != nil {\n\t\treturn err\n\t}\n\tec := yaml.NewEncoder(f)\n\tec.SetIndent(2)\n\n\treturn ec.Encode(content)\n}\n"
  },
  {
    "path": "internal/config/data/helpers_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage data_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSanitizeFileName(t *testing.T) {\n\tuu := map[string]struct {\n\t\tfile, e string\n\t}{\n\t\t\"empty\": {},\n\t\t\"plain\": {\n\t\t\tfile: \"bumble-bee-tuna\",\n\t\t\te:    \"bumble-bee-tuna\",\n\t\t},\n\t\t\"slash\": {\n\t\t\tfile: \"bumble/bee/tuna\",\n\t\t\te:    \"bumble-bee-tuna\",\n\t\t},\n\t\t\"column\": {\n\t\t\tfile: \"bumble::bee:tuna\",\n\t\t\te:    \"bumble-bee-tuna\",\n\t\t},\n\t\t\"eks\": {\n\t\t\tfile: \"arn:aws:eks:us-east-1:123456789:cluster/us-east-1-app-dev-common-eks\",\n\t\t\te:    \"arn-aws-eks-us-east-1-123456789-cluster-us-east-1-app-dev-common-eks\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, data.SanitizeFileName(u.file))\n\t\t})\n\t}\n}\n\nfunc TestHelperInList(t *testing.T) {\n\tuu := []struct {\n\t\titem     string\n\t\tlist     []string\n\t\texpected bool\n\t}{\n\t\t{\"a\", []string{}, false},\n\t\t{\"\", []string{}, false},\n\t\t{\"\", []string{\"\"}, true},\n\t\t{\"a\", []string{\"a\", \"b\", \"c\", \"d\"}, true},\n\t\t{\"z\", []string{\"a\", \"b\", \"c\", \"d\"}, false},\n\t}\n\n\tfor _, u := range uu {\n\t\tassert.Equal(t, u.expected, slices.Contains(u.list, u.item))\n\t}\n}\n\nfunc TestEnsureDirPathNone(t *testing.T) {\n\tconst mod = 0744\n\n\tdir := filepath.Join(os.TempDir(), \"k9s-test\")\n\t_ = os.Remove(dir)\n\n\tpath := filepath.Join(dir, \"duh.yaml\")\n\trequire.NoError(t, data.EnsureDirPath(path, mod))\n\n\tp, err := os.Stat(dir)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"drwxr--r--\", p.Mode().String())\n}\n\nfunc TestEnsureDirPathNoOpt(t *testing.T) {\n\tvar mod os.FileMode = 0744\n\tdir := filepath.Join(os.TempDir(), \"k9s-test\")\n\trequire.NoError(t, os.RemoveAll(dir))\n\trequire.NoError(t, os.Mkdir(dir, mod))\n\n\tpath := filepath.Join(dir, \"duh.yaml\")\n\trequire.NoError(t, data.EnsureDirPath(path, mod))\n\n\tp, err := os.Stat(dir)\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"drwxr--r--\", p.Mode().String())\n}\n"
  },
  {
    "path": "internal/config/data/ns.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage data\n\nimport (\n\t\"log/slog\"\n\t\"slices\"\n\t\"sync\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n)\n\nconst (\n\t// MaxFavoritesNS number # favorite namespaces to keep in the configuration.\n\tMaxFavoritesNS = 9\n)\n\n// Namespace tracks active and favorites namespaces.\ntype Namespace struct {\n\tActive        string   `yaml:\"active\"`\n\tLockFavorites bool     `yaml:\"lockFavorites\"`\n\tFavorites     []string `yaml:\"favorites\"`\n\tmx            sync.RWMutex\n}\n\n// NewNamespace create a new namespace configuration.\nfunc NewNamespace() *Namespace {\n\treturn NewActiveNamespace(client.DefaultNamespace)\n}\n\nfunc NewActiveNamespace(n string) *Namespace {\n\tif n == client.BlankNamespace {\n\t\tn = client.DefaultNamespace\n\t}\n\n\treturn &Namespace{\n\t\tActive:    n,\n\t\tFavorites: []string{client.DefaultNamespace},\n\t}\n}\n\nfunc (n *Namespace) merge(old *Namespace) {\n\tn.mx.Lock()\n\tdefer n.mx.Unlock()\n\n\tif n.LockFavorites {\n\t\treturn\n\t}\n\tfor _, fav := range old.Favorites {\n\t\tif slices.Contains(n.Favorites, fav) {\n\t\t\tcontinue\n\t\t}\n\t\tn.Favorites = append(n.Favorites, fav)\n\t}\n\n\tn.trimFavNs()\n}\n\n// Validate validates a namespace is setup correctly.\nfunc (n *Namespace) Validate(conn client.Connection) {\n\tn.mx.RLock()\n\tdefer n.mx.RUnlock()\n\n\tif conn == nil || !conn.IsValidNamespace(n.Active) {\n\t\treturn\n\t}\n\tfor _, ns := range n.Favorites {\n\t\tif !conn.IsValidNamespace(ns) {\n\t\t\tslog.Debug(\"Invalid favorite found\",\n\t\t\t\tslogs.Namespace, ns,\n\t\t\t\tslogs.AllNS, n.isAllNamespaces(),\n\t\t\t)\n\t\t\tn.rmFavNS(ns)\n\t\t}\n\t}\n\n\tn.trimFavNs()\n}\n\n// SetActive set the active namespace.\nfunc (n *Namespace) SetActive(ns string, _ KubeSettings) error {\n\tif n == nil {\n\t\tn = NewActiveNamespace(ns)\n\t}\n\n\tn.mx.Lock()\n\tdefer n.mx.Unlock()\n\n\tif ns == client.BlankNamespace {\n\t\tns = client.NamespaceAll\n\t}\n\tn.Active = ns\n\n\tif ns != \"\" && !n.LockFavorites {\n\t\tn.addFavNS(ns)\n\t}\n\n\treturn nil\n}\n\nfunc (n *Namespace) isAllNamespaces() bool {\n\treturn n.Active == client.NamespaceAll || n.Active == \"\"\n}\n\nfunc (n *Namespace) addFavNS(ns string) {\n\tif slices.Contains(n.Favorites, ns) {\n\t\treturn\n\t}\n\n\tnfv := make([]string, 0, MaxFavoritesNS)\n\tnfv = append(nfv, ns)\n\tfor i := range n.Favorites {\n\t\tif i+1 < MaxFavoritesNS {\n\t\t\tnfv = append(nfv, n.Favorites[i])\n\t\t}\n\t}\n\tn.Favorites = nfv\n}\n\nfunc (n *Namespace) rmFavNS(ns string) {\n\tif n.LockFavorites {\n\t\treturn\n\t}\n\n\tvictim := -1\n\tfor i, f := range n.Favorites {\n\t\tif f == ns {\n\t\t\tvictim = i\n\t\t\tbreak\n\t\t}\n\t}\n\tif victim < 0 {\n\t\treturn\n\t}\n\n\tn.Favorites = append(n.Favorites[:victim], n.Favorites[victim+1:]...)\n}\n\nfunc (n *Namespace) trimFavNs() {\n\tif len(n.Favorites) > MaxFavoritesNS {\n\t\tslog.Debug(\"Number of favorite exceeds hard limit. Trimming.\", slogs.Max, MaxFavoritesNS)\n\t\tn.Favorites = n.Favorites[:MaxFavoritesNS]\n\t}\n}\n"
  },
  {
    "path": "internal/config/data/ns_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage data_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/config/mock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNSValidate(t *testing.T) {\n\tns := data.NewNamespace()\n\tns.Validate(mock.NewMockConnection())\n\n\tassert.Equal(t, \"default\", ns.Active)\n\tassert.Equal(t, []string{\"default\"}, ns.Favorites)\n}\n\nfunc TestNSValidateMissing(t *testing.T) {\n\tns := data.NewNamespace()\n\tns.Validate(mock.NewMockConnection())\n\n\tassert.Equal(t, \"default\", ns.Active)\n\tassert.Equal(t, []string{\"default\"}, ns.Favorites)\n}\n\nfunc TestNSValidateNoNS(t *testing.T) {\n\tns := data.NewNamespace()\n\tns.Validate(mock.NewMockConnection())\n\n\tassert.Equal(t, \"default\", ns.Active)\n\tassert.Equal(t, []string{\"default\"}, ns.Favorites)\n}\n\nfunc TestNsValidateMaxNS(t *testing.T) {\n\tallNS := []string{\"ns9\", \"ns8\", \"ns7\", \"ns6\", \"ns5\", \"ns4\", \"ns3\", \"ns2\", \"ns1\", \"all\", \"default\"}\n\tns := data.NewNamespace()\n\tns.Favorites = allNS\n\n\tns.Validate(mock.NewMockConnection())\n\tassert.Len(t, ns.Favorites, data.MaxFavoritesNS)\n}\n\nfunc TestNSSetActive(t *testing.T) {\n\tallNS := []string{\"ns4\", \"ns3\", \"ns2\", \"ns1\", \"all\", \"default\"}\n\tuu := []struct {\n\t\tns  string\n\t\tfav []string\n\t}{\n\t\t{\"all\", []string{\"all\", \"default\"}},\n\t\t{\"ns1\", []string{\"ns1\", \"all\", \"default\"}},\n\t\t{\"ns2\", []string{\"ns2\", \"ns1\", \"all\", \"default\"}},\n\t\t{\"ns3\", []string{\"ns3\", \"ns2\", \"ns1\", \"all\", \"default\"}},\n\t\t{\"ns4\", allNS},\n\t}\n\n\tmk := mock.NewMockKubeSettings(makeFlags(\"cl-1\", \"ct-1\"))\n\tns := data.NewNamespace()\n\tfor _, u := range uu {\n\t\terr := ns.SetActive(u.ns, mk)\n\t\trequire.NoError(t, err)\n\t\tassert.Equal(t, u.ns, ns.Active)\n\t\tassert.Equal(t, u.fav, ns.Favorites)\n\t}\n}\n\nfunc TestNSValidateRmFavs(t *testing.T) {\n\tns := data.NewNamespace()\n\tns.Favorites = []string{\"default\", \"fred\"}\n\tns.Validate(mock.NewMockConnection())\n\n\tassert.Equal(t, []string{\"default\", \"fred\"}, ns.Favorites)\n}\n"
  },
  {
    "path": "internal/config/data/proxy.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage data\n\n// Proxy tracks a context's proxy configuration.\ntype Proxy struct {\n\tAddress string `yaml:\"address\"`\n}\n"
  },
  {
    "path": "internal/config/data/testdata/configs/aws_ct.yaml",
    "content": "k9s:\n  cluster: arn:aws:eks:eu-central-1:xxx:cluster/fred-blee\n  namespace:\n    active: default\n    lockFavorites: false\n    favorites:\n    - default\n  view:\n    active: po\n  featureGates:\n    nodeShell: false\n"
  },
  {
    "path": "internal/config/data/testdata/configs/ct-1-1.yaml",
    "content": "k9s:\n  cluster: cl-1\n  skin: skin-1\n  readOnly: false\n  namespace:\n    active: ns-1\n    lockFavorites: true\n    favorites:\n    - default\n    - ns-1\n    - ns-2\n  view:\n    active: dp\n  featureGates:\n    nodeShell: true\n"
  },
  {
    "path": "internal/config/data/testdata/configs/ct-1-2.yaml",
    "content": "k9s:\n  cluster: cl-1\n  skin: in_the_navy\n  readOnly: true\n  namespace:\n    active: default\n    lockFavorites: false\n    favorites:\n    - default\n  view:\n    active: po\n  featureGates:\n    nodeShell: false\n"
  },
  {
    "path": "internal/config/data/testdata/configs/ct-2-1.yaml",
    "content": "k9s:\n  cluster: cl-2\n  skin: skin-2\n  readOnly: true\n  namespace:\n    active: ns-2\n    lockFavorites: true\n    favorites:\n    - ns-1\n    - ns-2\n  view:\n    active: svc\n  featureGates:\n    nodeShell: true\n"
  },
  {
    "path": "internal/config/data/testdata/configs/def_ct.yaml",
    "content": "k9s:\n  cluster: cl-test\n  namespace:\n    active: default\n    lockFavorites: false\n    favorites:\n    - default\n  view:\n    active: po\n  featureGates:\n    nodeShell: false\n"
  },
  {
    "path": "internal/config/data/testdata/data/k9s/cl-1/ct-1-1/config.yaml",
    "content": "k9s:\n  cluster: cl-1\n  skin: skin-1\n  readOnly: false\n  namespace:\n    active: ns-1\n    lockFavorites: true\n    favorites:\n    - default\n    - ns-1\n    - ns-2\n  view:\n    active: dp\n  featureGates:\n    nodeShell: true\n"
  },
  {
    "path": "internal/config/data/testdata/data/k9s/cl-1/ct-1-2/config.yaml",
    "content": "k9s:\n  cluster: cl-1\n  skin: in_the_navy\n  readOnly: true\n  namespace:\n    active: default\n    lockFavorites: false\n    favorites:\n    - default\n  view:\n    active: po\n  featureGates:\n    nodeShell: false\n"
  },
  {
    "path": "internal/config/data/testdata/data/k9s/cl-2/ct-2-1/config.yaml",
    "content": "k9s:\n  cluster: cl-2\n  skin: skin-2\n  readOnly: true\n  namespace:\n    active: ns-2\n    lockFavorites: true\n    favorites:\n    - ns-1\n    - ns-2\n  view:\n    active: svc\n  featureGates:\n    nodeShell: true\n"
  },
  {
    "path": "internal/config/data/types.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage data\n\nimport (\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\n\t\"github.com/derailed/k9s/internal/config/json\"\n\t\"k8s.io/client-go/tools/clientcmd/api\"\n)\n\n// JSONValidator validate yaml configurations.\nvar JSONValidator = json.NewValidator()\n\nconst (\n\t// DefaultDirMod default unix perms for k9s directory.\n\tDefaultDirMod os.FileMode = 0744\n\n\t// DefaultFileMod default unix perms for k9s files.\n\tDefaultFileMod os.FileMode = 0600\n\n\t// MainConfigFile track main configuration file.\n\tMainConfigFile = \"config.yaml\"\n)\n\n// KubeSettings exposes kubeconfig context information.\ntype KubeSettings interface {\n\t// CurrentContextName returns the name of the current context.\n\tCurrentContextName() (string, error)\n\n\t// CurrentClusterName returns the name of the current cluster.\n\tCurrentClusterName() (string, error)\n\n\t// CurrentNamespaceName returns the name of the current namespace.\n\tCurrentNamespaceName() (string, error)\n\n\t// ContextNames returns all available context names.\n\tContextNames() (map[string]struct{}, error)\n\n\t// CurrentContext returns the current context configuration.\n\tCurrentContext() (*api.Context, error)\n\n\t// GetContext returns a given context configuration or err if not found.\n\tGetContext(string) (*api.Context, error)\n\n\t// SetProxy sets the proxy for the active context, if present\n\tSetProxy(proxy func(*http.Request) (*url.URL, error))\n}\n"
  },
  {
    "path": "internal/config/data/view.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage data\n\nconst DefaultView = \"po\"\n\n// View tracks view configuration options.\ntype View struct {\n\tActive string `yaml:\"active\"`\n}\n\n// NewView creates a new view configuration.\nfunc NewView() *View {\n\treturn &View{Active: DefaultView}\n}\n\n// Validate a view configuration.\nfunc (v *View) Validate() {\n\tif v.Active == \"\" {\n\t\tv.Active = DefaultView\n\t}\n}\n"
  },
  {
    "path": "internal/config/data/view_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage data_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestViewValidate(t *testing.T) {\n\tv := data.NewView()\n\n\tv.Validate()\n\tassert.Equal(t, \"po\", v.Active)\n\n\tv.Active = \"fred\"\n\tv.Validate()\n\tassert.Equal(t, \"fred\", v.Active)\n}\n\nfunc TestViewValidateBlank(t *testing.T) {\n\tvar v data.View\n\tv.Validate()\n\tassert.Equal(t, \"po\", v.Active)\n}\n"
  },
  {
    "path": "internal/config/files.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nimport (\n\t_ \"embed\"\n\t\"errors\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/adrg/xdg\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n)\n\nconst (\n\t// K9sEnvConfigDir represents k9s configuration dir env var.\n\tK9sEnvConfigDir = \"K9S_CONFIG_DIR\"\n\n\t// K9sEnvLogsDir represents k9s logs dir env var.\n\tK9sEnvLogsDir = \"K9S_LOGS_DIR\"\n\n\t// AppName tracks k9s app name.\n\tAppName = \"k9s\"\n\n\tK9sLogsFile = \"k9s.log\"\n)\n\nvar (\n\t//go:embed templates/benchmarks.yaml\n\t// benchmarkTpl tracks benchmark default config template\n\tbenchmarkTpl []byte\n\n\t//go:embed templates/aliases.yaml\n\t// aliasesTpl tracks aliases default config template\n\taliasesTpl []byte\n\n\t//go:embed templates/hotkeys.yaml\n\t// hotkeysTpl tracks hotkeys default config template\n\thotkeysTpl []byte\n\n\t//go:embed templates/stock-skin.yaml\n\t// stockSkinTpl tracks stock skin template\n\tstockSkinTpl []byte\n)\n\nvar (\n\t// AppConfigDir tracks main k9s config home directory.\n\tAppConfigDir string\n\n\t// AppSkinsDir tracks skins data directory.\n\tAppSkinsDir string\n\n\t// AppBenchmarksDir tracks benchmarks results directory.\n\tAppBenchmarksDir string\n\n\t// AppDumpsDir tracks screen dumps data directory.\n\tAppDumpsDir string\n\n\t// AppContextsDir tracks contexts data directory.\n\tAppContextsDir string\n\n\t// AppConfigFile tracks k9s config file.\n\tAppConfigFile string\n\n\t// AppLogFile tracks k9s logs file.\n\tAppLogFile string\n\n\t// AppViewsFile tracks custom views config file.\n\tAppViewsFile string\n\n\t// AppAliasesFile tracks aliases config file.\n\tAppAliasesFile string\n\n\t// AppPluginsFile tracks plugins config file.\n\tAppPluginsFile string\n\n\t// AppHotKeysFile tracks hotkeys config file.\n\tAppHotKeysFile string\n)\n\n// InitLogLoc initializes K9s logs location.\nfunc InitLogLoc() error {\n\tvar appLogDir string\n\tswitch {\n\tcase isEnvSet(K9sEnvLogsDir):\n\t\tappLogDir = os.Getenv(K9sEnvLogsDir)\n\tcase isEnvSet(K9sEnvConfigDir):\n\t\ttmpDir, err := UserTmpDir()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tappLogDir = tmpDir\n\tdefault:\n\t\tvar err error\n\t\tappLogDir, err = xdg.StateFile(AppName)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif err := data.EnsureFullPath(appLogDir, data.DefaultDirMod); err != nil {\n\t\treturn err\n\t}\n\tAppLogFile = filepath.Join(appLogDir, K9sLogsFile)\n\n\treturn nil\n}\n\n// InitLocs initializes k9s artifacts locations.\nfunc InitLocs() error {\n\tif isEnvSet(K9sEnvConfigDir) {\n\t\treturn initK9sEnvLocs()\n\t}\n\n\treturn initXDGLocs()\n}\n\nfunc initK9sEnvLocs() error {\n\tAppConfigDir = os.Getenv(K9sEnvConfigDir)\n\tif err := data.EnsureFullPath(AppConfigDir, data.DefaultDirMod); err != nil {\n\t\treturn err\n\t}\n\n\tAppDumpsDir = filepath.Join(AppConfigDir, \"screen-dumps\")\n\tif err := data.EnsureFullPath(AppDumpsDir, data.DefaultDirMod); err != nil {\n\t\tslog.Warn(\"Unable to create screen-dumps dir\", slogs.Dir, AppDumpsDir, slogs.Error, err)\n\t}\n\tAppBenchmarksDir = filepath.Join(AppConfigDir, \"benchmarks\")\n\tif err := data.EnsureFullPath(AppBenchmarksDir, data.DefaultDirMod); err != nil {\n\t\tslog.Warn(\"Unable to create benchmarks dir\",\n\t\t\tslogs.Dir, AppBenchmarksDir,\n\t\t\tslogs.Error, err,\n\t\t)\n\t}\n\tAppSkinsDir = filepath.Join(AppConfigDir, \"skins\")\n\tif err := data.EnsureFullPath(AppSkinsDir, data.DefaultDirMod); err != nil {\n\t\tslog.Warn(\"Unable to create skins dir\",\n\t\t\tslogs.Dir, AppSkinsDir,\n\t\t\tslogs.Error, err,\n\t\t)\n\t}\n\tAppContextsDir = filepath.Join(AppConfigDir, \"clusters\")\n\tif err := data.EnsureFullPath(AppContextsDir, data.DefaultDirMod); err != nil {\n\t\tslog.Warn(\"Unable to create clusters dir\",\n\t\t\tslogs.Dir, AppContextsDir,\n\t\t\tslogs.Error, err,\n\t\t)\n\t}\n\n\tAppConfigFile = filepath.Join(AppConfigDir, data.MainConfigFile)\n\tAppHotKeysFile = filepath.Join(AppConfigDir, \"hotkeys.yaml\")\n\tAppAliasesFile = filepath.Join(AppConfigDir, \"aliases.yaml\")\n\tAppPluginsFile = filepath.Join(AppConfigDir, \"plugins.yaml\")\n\tAppViewsFile = filepath.Join(AppConfigDir, \"views.yaml\")\n\n\treturn nil\n}\n\nfunc initXDGLocs() error {\n\tvar err error\n\n\tAppConfigDir, err = xdg.ConfigFile(AppName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tAppConfigFile, err = xdg.ConfigFile(filepath.Join(AppName, data.MainConfigFile))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tAppHotKeysFile = filepath.Join(AppConfigDir, \"hotkeys.yaml\")\n\tAppAliasesFile = filepath.Join(AppConfigDir, \"aliases.yaml\")\n\tAppPluginsFile = filepath.Join(AppConfigDir, \"plugins.yaml\")\n\tAppViewsFile = filepath.Join(AppConfigDir, \"views.yaml\")\n\n\tAppSkinsDir = filepath.Join(AppConfigDir, \"skins\")\n\tif e := data.EnsureFullPath(AppSkinsDir, data.DefaultDirMod); e != nil {\n\t\tslog.Warn(\"No skins dir detected\", slogs.Error, e)\n\t}\n\n\tAppDumpsDir, err = xdg.StateFile(filepath.Join(AppName, \"screen-dumps\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tAppBenchmarksDir, err = xdg.StateFile(filepath.Join(AppName, \"benchmarks\"))\n\tif err != nil {\n\t\tslog.Warn(\"No benchmarks dir detected\",\n\t\t\tslogs.Dir, AppBenchmarksDir,\n\t\t\tslogs.Error, err,\n\t\t)\n\t}\n\n\tdataDir, err := xdg.DataFile(AppName)\n\tif err != nil {\n\t\treturn err\n\t}\n\tAppContextsDir = filepath.Join(dataDir, \"clusters\")\n\tif err := data.EnsureFullPath(AppContextsDir, data.DefaultDirMod); err != nil {\n\t\tslog.Warn(\"No context dir detected\",\n\t\t\tslogs.Dir, AppContextsDir,\n\t\t\tslogs.Error, err,\n\t\t)\n\t}\n\n\treturn nil\n}\n\n// AppContextDir generates a valid context config dir.\nfunc AppContextDir(cluster, context string) string {\n\treturn filepath.Join(AppContextsDir, data.SanitizeContextSubpath(cluster, context))\n}\n\n// AppContextAliasesFile generates a valid context specific aliases file path.\nfunc AppContextAliasesFile(cluster, context string) string {\n\treturn filepath.Join(AppContextsDir, data.SanitizeContextSubpath(cluster, context), \"aliases.yaml\")\n}\n\n// AppContextPluginsFile generates a valid context specific plugins file path.\nfunc AppContextPluginsFile(cluster, context string) string {\n\treturn filepath.Join(AppContextsDir, data.SanitizeContextSubpath(cluster, context), \"plugins.yaml\")\n}\n\n// AppContextHotkeysFile generates a valid context specific hotkeys file path.\nfunc AppContextHotkeysFile(cluster, context string) string {\n\treturn filepath.Join(AppContextsDir, data.SanitizeContextSubpath(cluster, context), \"hotkeys.yaml\")\n}\n\n// AppContextConfig generates a valid context config file path.\nfunc AppContextConfig(cluster, context string) string {\n\treturn filepath.Join(AppContextDir(cluster, context), data.MainConfigFile)\n}\n\n// DumpsDir generates a valid context dump directory.\nfunc DumpsDir(cluster, context string) (string, error) {\n\tdir := filepath.Join(AppDumpsDir, data.SanitizeContextSubpath(cluster, context))\n\n\treturn dir, data.EnsureDirPath(dir, data.DefaultDirMod)\n}\n\n// EnsureBenchmarksDir generates a valid benchmark results directory.\nfunc EnsureBenchmarksDir(cluster, context string) (string, error) {\n\tdir := filepath.Join(AppBenchmarksDir, data.SanitizeContextSubpath(cluster, context))\n\n\treturn dir, data.EnsureDirPath(dir, data.DefaultDirMod)\n}\n\n// EnsureBenchmarksCfgFile generates a valid benchmark file.\nfunc EnsureBenchmarksCfgFile(cluster, context string) (string, error) {\n\tf := filepath.Join(AppContextDir(cluster, context), \"benchmarks.yaml\")\n\tif err := data.EnsureDirPath(f, data.DefaultDirMod); err != nil {\n\t\treturn \"\", err\n\t}\n\tif _, err := os.Stat(f); errors.Is(err, fs.ErrNotExist) {\n\t\treturn f, os.WriteFile(f, benchmarkTpl, data.DefaultFileMod)\n\t}\n\n\treturn f, nil\n}\n\n// EnsureAliasesCfgFile generates a valid aliases file.\nfunc EnsureAliasesCfgFile() (string, error) {\n\tf := filepath.Join(AppConfigDir, \"aliases.yaml\")\n\tif err := data.EnsureDirPath(f, data.DefaultDirMod); err != nil {\n\t\treturn \"\", err\n\t}\n\tif _, err := os.Stat(f); errors.Is(err, fs.ErrNotExist) {\n\t\treturn f, os.WriteFile(f, aliasesTpl, data.DefaultFileMod)\n\t}\n\n\treturn f, nil\n}\n\n// EnsureHotkeysCfgFile generates a valid hotkeys file.\nfunc EnsureHotkeysCfgFile() (string, error) {\n\tf := filepath.Join(AppConfigDir, \"hotkeys.yaml\")\n\tif err := data.EnsureDirPath(f, data.DefaultDirMod); err != nil {\n\t\treturn \"\", err\n\t}\n\tif _, err := os.Stat(f); errors.Is(err, fs.ErrNotExist) {\n\t\treturn f, os.WriteFile(f, hotkeysTpl, data.DefaultFileMod)\n\t}\n\n\treturn f, nil\n}\n\n// SkinFileFromName generate skin file path from spec.\nfunc SkinFileFromName(n string) string {\n\tif n == \"\" {\n\t\tn = \"stock\"\n\t}\n\n\treturn filepath.Join(AppSkinsDir, n+\".yaml\")\n}\n"
  },
  {
    "path": "internal/config/files_int_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/adrg/xdg\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_initXDGLocs(t *testing.T) {\n\ttmp, err := UserTmpDir()\n\trequire.NoError(t, err)\n\n\trequire.NoError(t, os.Unsetenv(\"XDG_CONFIG_HOME\"))\n\trequire.NoError(t, os.Unsetenv(\"XDG_CACHE_HOME\"))\n\trequire.NoError(t, os.Unsetenv(\"XDG_STATE_HOME\"))\n\trequire.NoError(t, os.Unsetenv(\"XDG_DATA_HOME\"))\n\n\trequire.NoError(t, os.Setenv(\"XDG_CONFIG_HOME\", filepath.Join(tmp, \"k9s-xdg\", \"config\")))\n\trequire.NoError(t, os.Setenv(\"XDG_CACHE_HOME\", filepath.Join(tmp, \"k9s-xdg\", \"cache\")))\n\trequire.NoError(t, os.Setenv(\"XDG_STATE_HOME\", filepath.Join(tmp, \"k9s-xdg\", \"state\")))\n\trequire.NoError(t, os.Setenv(\"XDG_DATA_HOME\", filepath.Join(tmp, \"k9s-xdg\", \"data\")))\n\txdg.Reload()\n\n\tuu := map[string]struct {\n\t\tconfigDir          string\n\t\tconfigFile         string\n\t\tbenchmarksDir      string\n\t\tcontextsDir        string\n\t\tcontextHotkeysFile string\n\t\tcontextConfig      string\n\t\tdumpsDir           string\n\t\tbenchDir           string\n\t\thkFile             string\n\t}{\n\t\t\"check-env\": {\n\t\t\tconfigDir:          filepath.Join(tmp, \"k9s-xdg\", \"config\", \"k9s\"),\n\t\t\tconfigFile:         filepath.Join(tmp, \"k9s-xdg\", \"config\", \"k9s\", data.MainConfigFile),\n\t\t\tbenchmarksDir:      filepath.Join(tmp, \"k9s-xdg\", \"state\", \"k9s\", \"benchmarks\"),\n\t\t\tcontextsDir:        filepath.Join(tmp, \"k9s-xdg\", \"data\", \"k9s\", \"clusters\"),\n\t\t\tcontextHotkeysFile: filepath.Join(tmp, \"k9s-xdg\", \"data\", \"k9s\", \"clusters\", \"cl-1\", \"ct-1-1\", \"hotkeys.yaml\"),\n\t\t\tcontextConfig:      filepath.Join(tmp, \"k9s-xdg\", \"data\", \"k9s\", \"clusters\", \"cl-1\", \"ct-1-1\", data.MainConfigFile),\n\t\t\tdumpsDir:           filepath.Join(tmp, \"k9s-xdg\", \"state\", \"k9s\", \"screen-dumps\", \"cl-1\", \"ct-1-1\"),\n\t\t\tbenchDir:           filepath.Join(tmp, \"k9s-xdg\", \"state\", \"k9s\", \"benchmarks\", \"cl-1\", \"ct-1-1\"),\n\t\t\thkFile:             filepath.Join(tmp, \"k9s-xdg\", \"config\", \"k9s\", \"hotkeys.yaml\"),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\trequire.NoError(t, initXDGLocs())\n\t\t\tassert.Equal(t, u.configDir, AppConfigDir)\n\t\t\tassert.Equal(t, u.configFile, AppConfigFile)\n\t\t\tassert.Equal(t, u.benchmarksDir, AppBenchmarksDir)\n\t\t\tassert.Equal(t, u.contextsDir, AppContextsDir)\n\t\t\tassert.Equal(t, u.contextHotkeysFile, AppContextHotkeysFile(\"cl-1\", \"ct-1-1\"))\n\t\t\tassert.Equal(t, u.contextConfig, AppContextConfig(\"cl-1\", \"ct-1-1\"))\n\t\t\tdir, err := DumpsDir(\"cl-1\", \"ct-1-1\")\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, u.dumpsDir, dir)\n\t\t\tbdir, err := EnsureBenchmarksDir(\"cl-1\", \"ct-1-1\")\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, u.benchDir, bdir)\n\t\t\thk, err := EnsureHotkeysCfgFile()\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, u.hkFile, hk)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/config/files_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/adrg/xdg\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestInitLogLoc(t *testing.T) {\n\ttmp, err := config.UserTmpDir()\n\trequire.NoError(t, err)\n\n\tuu := map[string]struct {\n\t\tdir string\n\t\te   string\n\t}{\n\t\t\"log-env\": {\n\t\t\tdir: \"/tmp/test/k9s/logs\",\n\t\t\te:   \"/tmp/test/k9s/logs/k9s.log\",\n\t\t},\n\t\t\"xdg-env\": {\n\t\t\tdir: \"/tmp/test/xdg-state\",\n\t\t\te:   \"/tmp/test/xdg-state/k9s/k9s.log\",\n\t\t},\n\t\t\"cfg-env\": {\n\t\t\tdir: \"/tmp/test/k9s-test\",\n\t\t\te:   filepath.Join(tmp, \"k9s.log\"),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\trequire.NoError(t, os.Unsetenv(config.K9sEnvLogsDir))\n\t\t\trequire.NoError(t, os.Unsetenv(\"XDG_STATE_HOME\"))\n\t\t\trequire.NoError(t, os.Unsetenv(config.K9sEnvConfigDir))\n\t\t\tswitch k {\n\t\t\tcase \"log-env\":\n\t\t\t\trequire.NoError(t, os.Setenv(config.K9sEnvLogsDir, u.dir))\n\t\t\tcase \"xdg-env\":\n\t\t\t\trequire.NoError(t, os.Setenv(\"XDG_STATE_HOME\", u.dir))\n\t\t\t\txdg.Reload()\n\t\t\tcase \"cfg-env\":\n\t\t\t\trequire.NoError(t, os.Setenv(config.K9sEnvConfigDir, u.dir))\n\t\t\t}\n\t\t\terr := config.InitLogLoc()\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, u.e, config.AppLogFile)\n\t\t\trequire.NoError(t, os.RemoveAll(config.AppLogFile))\n\t\t})\n\t}\n}\n\nfunc TestEnsureBenchmarkCfg(t *testing.T) {\n\trequire.NoError(t, os.Setenv(config.K9sEnvConfigDir, \"/tmp/test-config\"))\n\trequire.NoError(t, config.InitLocs())\n\tdefer require.NoError(t, os.RemoveAll(\"/tmp/test-config\"))\n\n\trequire.NoError(t, data.EnsureFullPath(\"/tmp/test-config/clusters/cl-1/ct-2\", data.DefaultDirMod))\n\trequire.NoError(t, os.WriteFile(\"/tmp/test-config/clusters/cl-1/ct-2/benchmarks.yaml\", []byte{}, data.DefaultFileMod))\n\n\tuu := map[string]struct {\n\t\tcluster, context string\n\t\tf, e             string\n\t}{\n\t\t\"not-exist\": {\n\t\t\tcluster: \"cl-1\",\n\t\t\tcontext: \"ct-1\",\n\t\t\tf:       \"/tmp/test-config/clusters/cl-1/ct-1/benchmarks.yaml\",\n\t\t\te:       \"benchmarks:\\n  defaults:\\n    concurrency: 2\\n    requests: 200\",\n\t\t},\n\t\t\"exist\": {\n\t\t\tcluster: \"cl-1\",\n\t\t\tcontext: \"ct-2\",\n\t\t\tf:       \"/tmp/test-config/clusters/cl-1/ct-2/benchmarks.yaml\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tf, err := config.EnsureBenchmarksCfgFile(u.cluster, u.context)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, u.f, f)\n\t\t\tbb, err := os.ReadFile(f)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, u.e, string(bb))\n\t\t})\n\t}\n}\n\nfunc TestSkinFileFromName(t *testing.T) {\n\tconfig.AppSkinsDir = \"/tmp/k9s-test/skins\"\n\tdefer require.NoError(t, os.RemoveAll(\"/tmp/k9s-test/skins\"))\n\n\tuu := map[string]struct {\n\t\tn string\n\t\te string\n\t}{\n\t\t\"empty\": {\n\t\t\te: \"/tmp/k9s-test/skins/stock.yaml\",\n\t\t},\n\t\t\"happy\": {\n\t\t\tn: \"fred-blee\",\n\t\t\te: \"/tmp/k9s-test/skins/fred-blee.yaml\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, config.SkinFileFromName(u.n))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/config/flags.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nconst (\n\t// DefaultRefreshRate represents the refresh interval.\n\tDefaultRefreshRate float32 = 2.0 // secs\n\n\t// DefaultLogLevel represents the default log level.\n\tDefaultLogLevel = \"info\"\n\n\t// DefaultCommand represents the default command to run.\n\tDefaultCommand = \"\"\n)\n\n// Flags represents K9s configuration flags.\ntype Flags struct {\n\tRefreshRate   *float32\n\tLogLevel      *string\n\tLogFile       *string\n\tHeadless      *bool\n\tLogoless      *bool\n\tCommand       *string\n\tAllNamespaces *bool\n\tReadOnly      *bool\n\tWrite         *bool\n\tCrumbsless    *bool\n\tSplashless    *bool\n\tInvert        *bool\n\tScreenDumpDir *string\n}\n\n// NewFlags returns new configuration flags.\nfunc NewFlags() *Flags {\n\treturn &Flags{\n\t\tRefreshRate:   float32Ptr(DefaultRefreshRate),\n\t\tLogLevel:      strPtr(DefaultLogLevel),\n\t\tLogFile:       strPtr(AppLogFile),\n\t\tHeadless:      boolPtr(false),\n\t\tLogoless:      boolPtr(false),\n\t\tCommand:       strPtr(DefaultCommand),\n\t\tAllNamespaces: boolPtr(false),\n\t\tReadOnly:      boolPtr(false),\n\t\tWrite:         boolPtr(false),\n\t\tCrumbsless:    boolPtr(false),\n\t\tSplashless:    boolPtr(false),\n\t\tInvert:        boolPtr(false),\n\t\tScreenDumpDir: strPtr(AppDumpsDir),\n\t}\n}\n\nfunc boolPtr(b bool) *bool {\n\treturn &b\n}\n\nfunc float32Ptr(f float32) *float32 {\n\treturn &f\n}\n\nfunc strPtr(s string) *string {\n\treturn &s\n}\n"
  },
  {
    "path": "internal/config/flags_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNewFlags(t *testing.T) {\n\tconfig.AppDumpsDir = \"/tmp/k9s-test/screen-dumps\"\n\tconfig.AppLogFile = \"/tmp/k9s-test/k9s.log\"\n\n\tf := config.NewFlags()\n\tassert.InDelta(t, 2.0, *f.RefreshRate, 0.001)\n\tassert.Equal(t, \"info\", *f.LogLevel)\n\tassert.Equal(t, \"/tmp/k9s-test/k9s.log\", *f.LogFile)\n\tassert.Equal(t, config.AppDumpsDir, *f.ScreenDumpDir)\n\tassert.Empty(t, *f.Command)\n\tassert.False(t, *f.Headless)\n\tassert.False(t, *f.Logoless)\n\tassert.False(t, *f.AllNamespaces)\n\tassert.False(t, *f.ReadOnly)\n\tassert.False(t, *f.Write)\n\tassert.False(t, *f.Crumbsless)\n\tassert.False(t, *f.Splashless)\n}\n"
  },
  {
    "path": "internal/config/helpers.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nimport (\n\t\"log/slog\"\n\t\"os\"\n\t\"os/user\"\n\t\"path/filepath\"\n\n\t\"github.com/derailed/k9s/internal/slogs\"\n)\n\nconst (\n\tenvPFAddress          = \"K9S_DEFAULT_PF_ADDRESS\"\n\tdefaultPortFwdAddress = \"localhost\"\n)\n\n// IsBoolSet checks if a bool ptr is set.\nfunc IsBoolSet(b *bool) bool {\n\treturn b != nil && *b\n}\n\nfunc isStringSet(s *string) bool {\n\treturn s != nil && *s != \"\"\n}\n\nfunc isYamlFile(file string) bool {\n\text := filepath.Ext(file)\n\n\treturn ext == \".yml\" || ext == \".yaml\"\n}\n\n// isEnvSet checks if env var is set.\nfunc isEnvSet(env string) bool {\n\treturn os.Getenv(env) != \"\"\n}\n\n// UserTmpDir returns the temp dir with the current user name.\nfunc UserTmpDir() (string, error) {\n\tu, err := user.Current()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdir := filepath.Join(os.TempDir(), u.Username, AppName)\n\n\treturn dir, nil\n}\n\n// MustK9sUser establishes current user identity or fail.\nfunc MustK9sUser() string {\n\tusr, err := user.Current()\n\tif err != nil {\n\t\tenvUsr := os.Getenv(\"USER\")\n\t\tif envUsr != \"\" {\n\t\t\treturn envUsr\n\t\t}\n\t\tenvUsr = os.Getenv(\"LOGNAME\")\n\t\tif envUsr != \"\" {\n\t\t\treturn envUsr\n\t\t}\n\t\tslog.Error(\"Die on retrieving user info\", slogs.Error, err)\n\t\tos.Exit(1)\n\t}\n\treturn usr.Username\n}\n\nfunc defaultPFAddress() string {\n\tif a := os.Getenv(envPFAddress); a != \"\" {\n\t\treturn a\n\t}\n\n\treturn defaultPortFwdAddress\n}\n"
  },
  {
    "path": "internal/config/hotkey.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nimport (\n\t\"errors\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os\"\n\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/config/json\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"gopkg.in/yaml.v3\"\n)\n\n// HotKeys represents a collection of plugins.\ntype HotKeys struct {\n\tHotKey map[string]HotKey `yaml:\"hotKeys\"`\n}\n\n// HotKey describes a K9s hotkey.\ntype HotKey struct {\n\tShortCut    string `yaml:\"shortCut\"`\n\tOverride    bool   `yaml:\"override\"`\n\tDescription string `yaml:\"description\"`\n\tCommand     string `yaml:\"command\"`\n\tKeepHistory bool   `yaml:\"keepHistory\"`\n}\n\n// NewHotKeys returns a new plugin.\nfunc NewHotKeys() HotKeys {\n\treturn HotKeys{\n\t\tHotKey: make(map[string]HotKey),\n\t}\n}\n\n// Load K9s plugins.\nfunc (h HotKeys) Load(path string) error {\n\tif err := h.LoadHotKeys(AppHotKeysFile); err != nil {\n\t\treturn err\n\t}\n\tif _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {\n\t\treturn nil\n\t}\n\n\treturn h.LoadHotKeys(path)\n}\n\n// LoadHotKeys loads plugins from a given file.\nfunc (h HotKeys) LoadHotKeys(path string) error {\n\tif _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {\n\t\treturn nil\n\t}\n\tbb, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := data.JSONValidator.Validate(json.HotkeysSchema, bb); err != nil {\n\t\tslog.Warn(\"Validation failed. Please update your config and restart.\",\n\t\t\tslogs.Path, path,\n\t\t\tslogs.Error, err,\n\t\t)\n\t}\n\n\tvar hh HotKeys\n\tif err := yaml.Unmarshal(bb, &hh); err != nil {\n\t\treturn err\n\t}\n\tfor k, v := range hh.HotKey {\n\t\th.HotKey[k] = v\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/config/hotkey_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestHotKeyLoad(t *testing.T) {\n\th := config.NewHotKeys()\n\trequire.NoError(t, h.LoadHotKeys(\"testdata/hotkeys/hotkeys.yaml\"))\n\tassert.Len(t, h.HotKey, 1)\n\n\tk, ok := h.HotKey[\"pods\"]\n\tassert.True(t, ok)\n\tassert.Equal(t, \"shift-0\", k.ShortCut)\n\tassert.Equal(t, \"Launch pod view\", k.Description)\n\tassert.Equal(t, \"pods\", k.Command)\n\tassert.True(t, k.KeepHistory)\n}\n"
  },
  {
    "path": "internal/config/json/schemas/aliases.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"K9s aliases schema\",\n  \"type\": \"object\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"aliases\": {\n      \"type\": \"object\",\n      \"additionalProperties\": { \"type\": \"string\" },\n      \"required\": []\n    }\n  },\n  \"required\": [\"aliases\"]\n}\n"
  },
  {
    "path": "internal/config/json/schemas/context.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"K9s context config schema\",\n  \"type\": \"object\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"k9s\": {\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"cluster\": { \"type\": \"string\" },\n        \"readOnly\": {\"type\": \"boolean\"},\n        \"skin\": { \"type\": \"string\" },\n        \"proxy\": {\n          \"oneOf\": [\n            { \"type\": \"null\" },\n            {\n              \"type\": \"object\",\n              \"additionalProperties\": false,\n              \"properties\":\n              {\n                \"address\": {\"type\": \"string\"}\n              }\n            }\n          ]\n        },\n        \"namespace\": {\n          \"type\": \"object\",\n          \"additionalProperties\": false,\n          \"properties\": {\n            \"active\": {\"type\": \"string\"},\n            \"lockFavorites\": {\"type\": \"boolean\"},\n            \"favorites\": {\n              \"type\": \"array\",\n              \"items\": {\"type\": \"string\"}\n            }\n          }\n        },\n        \"view\": {\n          \"type\": \"object\",\n          \"additionalProperties\": false,\n          \"properties\": {\n            \"active\": { \"type\": \"string\" }\n          }\n        },\n        \"featureGates\": {\n          \"type\": \"object\",\n          \"additionalProperties\": false,\n          \"properties\": {\n            \"nodeShell\": { \"type\": \"boolean\" }\n          }\n        }\n      }\n    }\n  },\n  \"required\": [\"k9s\"]\n}\n"
  },
  {
    "path": "internal/config/json/schemas/hotkeys.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"K9s hotkeys schema\",\n  \"type\": \"object\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"hotKeys\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"shortCut\": {\"type\": \"string\"},\n          \"override\": { \"type\": \"boolean\" },\n          \"description\": {\"type\": \"string\"},\n          \"command\": {\"type\": \"string\"},\n          \"keepHistory\": {\"type\": \"boolean\"}\n        }\n      }\n    }\n  },\n  \"required\": [\"hotKeys\"]\n}\n"
  },
  {
    "path": "internal/config/json/schemas/k9s.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"K9s config schema\",\n  \"type\": \"object\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"k9s\": {\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"liveViewAutoRefresh\": { \"type\": \"boolean\" },\n        \"gpuVendors\": {\n          \"type\": \"object\",\n          \"additionalProperties\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"vendor\": { \"type\": \"string\" },\n              \"model\": { \"type\": \"string\" }\n            },\n            \"required\": [\"vendor\", \"model\"]\n          }\n        },\n        \"screenDumpDir\": {\"type\": \"string\"},\n        \"refreshRate\": { \"type\": \"number\" },\n        \"apiServerTimeout\": { \"type\": \"string\" },\n        \"maxConnRetry\": { \"type\": \"integer\" },\n        \"readOnly\": { \"type\": \"boolean\" },\n        \"noExitOnCtrlC\": { \"type\": \"boolean\" },\n        \"skipLatestRevCheck\": { \"type\": \"boolean\" },\n        \"disablePodCounting\": { \"type\": \"boolean\" },\n        \"defaultView\": { \"type\": \"string\" },\n        \"portForwardAddress\": { \"type\": \"string\" },\n        \"ui\": {\n          \"type\": \"object\",\n          \"additionalProperties\": false,\n          \"properties\": {\n            \"enableMouse\": {\"type\": \"boolean\"},\n            \"headless\": {\"type\": \"boolean\"},\n            \"logoless\": {\"type\": \"boolean\"},\n            \"crumbsless\": {\"type\": \"boolean\"},\n            \"splashless\": {\"type\": \"boolean\"},\n            \"noIcons\": {\"type\": \"boolean\"},\n            \"reactive\": {\"type\": \"boolean\"},\n            \"skin\": {\"type\": \"string\"},\n            \"defaultsToFullScreen\": {\"type\": \"boolean\"},\n            \"useFullGVRTitle\": {\"type\": \"boolean\"},\n            \"invert\": {\"type\": \"boolean\"}\n          }\n        },\n        \"shellPod\": {\n          \"type\": \"object\",\n          \"additionalProperties\": true,\n          \"properties\": {\n            \"image\": { \"type\": \"string\" },\n            \"command\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"string\"}\n            },\n            \"args\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"string\"}\n            },\n            \"namespace\": { \"type\": \"string\" },\n            \"limits\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"cpu\": { \"type\": \"string\" },\n                \"memory\": { \"type\": \"string\" }\n              },\n              \"required\": [\"cpu\", \"memory\"]\n            },\n            \"labels\": {\n              \"type\": \"object\",\n              \"additionalProperties\": { \"type\": \"string\" },\n              \"required\": []\n            },\n            \"tty\": { \"type\": \"boolean\" },\n            \"imagePullPolicy\": { \"type\": \"string\" },\n            \"imagePullSecrets\": {\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"name\": { \"type\": \"string\" }\n                }\n              }\n            }\n          },\n          \"required\": [\"image\", \"namespace\", \"limits\"]\n        },\n        \"imageScans\": {\n          \"type\": \"object\",\n          \"additionalProperties\": false,\n          \"properties\": {\n            \"enable\": { \"type\": \"boolean\" },\n            \"namespace\": { \"type\": \"string\" },\n            \"exclusions\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"namespaces\": {\n                  \"type\": \"array\",\n                  \"items\": { \"type\": \"string\" }\n                },\n                \"labels\": {\n                  \"type\": \"object\",\n                  \"additionalProperties\": {\n                    \"type\": \"array\",\n                    \"items\": { \"type\": \"string\" }\n                  }\n                }\n              }\n            }\n          },\n          \"required\": [\"enable\"]\n        },\n        \"logger\": {\n          \"type\": \"object\",\n          \"additionalProperties\": false,\n          \"properties\": {\n            \"tail\": {\"type\": \"integer\"},\n            \"buffer\": {\"type\": \"integer\"},\n            \"sinceSeconds\": {\"type\": \"integer\"},\n            \"textWrap\": {\"type\": \"boolean\"},\n            \"disableAutoscroll\": {\"type\": \"boolean\"},\n            \"columnLock\": {\"type\": \"boolean\"},\n            \"showTime\": {\"type\": \"boolean\"}\n          }\n        },\n        \"thresholds\": {\n          \"type\": \"object\",\n          \"additionalProperties\": false,\n          \"properties\": {\n            \"cpu\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"critical\": {\"type\": \"integer\"},\n                \"warn\": {\"type\": \"integer\"}\n              }\n            },\n            \"memory\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"critical\": {\"type\": \"integer\"},\n                \"warn\": {\"type\": \"integer\"}\n              }\n            }\n          }\n        }\n      }\n    }\n  },\n  \"required\": [\"k9s\"]\n}\n"
  },
  {
    "path": "internal/config/json/schemas/plugin-multi.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"K9s plugin-multi schema\",\n  \"type\": \"object\",\n  \"additionalProperties\": {\n    \"properties\": {\n      \"shortCut\": { \"type\": \"string\" },\n      \"override\": { \"type\": \"boolean\" },\n      \"description\": { \"type\": \"string\" },\n      \"confirm\": { \"type\": \"boolean\" },\n      \"dangerous\": { \"type\": \"boolean\" },\n      \"scopes\": {\n        \"type\": \"array\",\n        \"items\": { \"type\": \"string\" }\n      },\n      \"command\": { \"type\": \"string\" },\n      \"background\": { \"type\": \"boolean\" },\n      \"overwriteOutput\": { \"type\": \"boolean\" },\n      \"args\": {\n        \"type\": \"array\",\n        \"items\": { \"type\": [\"string\", \"number\"] }\n      },\n      \"inputs\": {\n        \"type\": \"array\",\n        \"maxItems\": 5,\n        \"items\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"name\": { \"type\": \"string\" },\n            \"label\": { \"type\": \"string\" },\n            \"type\": { \"type\": \"string\", \"enum\": [\"string\", \"number\", \"bool\", \"dropdown\"] },\n            \"required\": { \"type\": \"boolean\" },\n            \"default\": { \"type\": [\"string\", \"number\", \"boolean\"] },\n            \"options\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"string\" }\n            }\n          },\n          \"required\": [\"name\", \"type\"],\n          \"if\": { \"required\": [\"default\"] },\n          \"then\": {\n            \"properties\": { \"required\": { \"const\": true } },\n            \"required\": [\"required\"]\n          }\n        }\n      }\n    },\n    \"required\": [\"shortCut\", \"description\", \"scopes\", \"command\"]\n  }\n}\n"
  },
  {
    "path": "internal/config/json/schemas/plugin.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"K9s plugin schema\",\n  \"type\": \"object\",\n  \"additionalProperties\": false,\n  \"properties\": {\n      \"shortCut\": { \"type\": \"string\" },\n      \"override\": { \"type\": \"boolean\" },\n      \"description\": { \"type\": \"string\" },\n      \"confirm\": { \"type\": \"boolean\" },\n      \"dangerous\": { \"type\": \"boolean\" },\n      \"scopes\": {\n        \"type\": \"array\",\n        \"items\": { \"type\": \"string\" }\n      },\n      \"command\": { \"type\": \"string\" },\n      \"background\": { \"type\": \"boolean\" },\n      \"overwriteOutput\": { \"type\": \"boolean\" },\n      \"args\": {\n        \"type\": \"array\",\n        \"items\": { \"type\": [\"string\", \"number\"] }\n      },\n      \"inputs\": {\n        \"type\": \"array\",\n        \"maxItems\": 5,\n        \"items\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"name\": { \"type\": \"string\" },\n            \"label\": { \"type\": \"string\" },\n            \"type\": { \"type\": \"string\", \"enum\": [\"string\", \"number\", \"bool\", \"dropdown\"] },\n            \"required\": { \"type\": \"boolean\" },\n            \"default\": { \"type\": [\"string\", \"number\", \"boolean\"] },\n            \"options\": {\n              \"type\": \"array\",\n              \"items\": { \"type\": \"string\" }\n            }\n          },\n          \"required\": [\"name\", \"type\"],\n          \"if\": { \"required\": [\"default\"] },\n          \"then\": {\n            \"properties\": { \"required\": { \"const\": true } },\n            \"required\": [\"required\"]\n          }\n        }\n      }\n  },\n  \"required\": [\"shortCut\", \"description\", \"scopes\", \"command\"]\n}\n"
  },
  {
    "path": "internal/config/json/schemas/plugins.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"K9s plugins schema\",\n  \"type\": \"object\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"plugins\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"properties\": {\n          \"shortCut\": { \"type\": \"string\" },\n          \"override\": { \"type\": \"boolean\" },\n          \"description\": { \"type\": \"string\" },\n          \"confirm\": { \"type\": \"boolean\" },\n          \"dangerous\": { \"type\": \"boolean\" },\n          \"scopes\": {\n            \"type\": \"array\",\n            \"items\": { \"type\": \"string\" }\n          },\n          \"command\": { \"type\": \"string\" },\n          \"background\": { \"type\": \"boolean\" },\n          \"overwriteOutput\": { \"type\": \"boolean\" },\n          \"args\": {\n            \"type\": \"array\",\n            \"items\": { \"type\": [\"string\", \"number\"] }\n          },\n          \"inputs\": {\n            \"type\": \"array\",\n            \"maxItems\": 5,\n            \"items\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"name\": { \"type\": \"string\" },\n                \"label\": { \"type\": \"string\" },\n                \"type\": { \"type\": \"string\", \"enum\": [\"string\", \"number\", \"bool\", \"dropdown\"] },\n                \"required\": { \"type\": \"boolean\" },\n                \"default\": { \"type\": [\"string\", \"number\", \"boolean\"] },\n                \"options\": {\n                  \"type\": \"array\",\n                  \"items\": { \"type\": \"string\" }\n                }\n              },\n              \"required\": [\"name\", \"type\"],\n              \"if\": { \"required\": [\"default\"] },\n              \"then\": {\n                \"properties\": { \"required\": { \"const\": true } },\n                \"required\": [\"required\"]\n              }\n            }\n          }\n        },\n        \"required\": [\"shortCut\", \"description\", \"scopes\", \"command\"]\n      },\n      \"required\": []\n    }\n  },\n  \"required\": [\"plugins\"]\n}\n"
  },
  {
    "path": "internal/config/json/schemas/skin.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"K9s skin schema\",\n  \"type\": \"object\",\n  \"additionalProperties\": true,\n  \"properties\": {\n    \"k9s\": {\n      \"type\": \"object\",\n      \"additionalProperties\": false,\n      \"properties\": {\n        \"body\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"fgColor\": {\"type\": \"string\"},\n            \"bgColor\": {\"type\": \"string\"},\n            \"logoColor\": {\"type\": \"string\"}\n          }\n        },\n        \"prompt\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"fgColor\": {\"type\": \"string\"},\n            \"bgColor\": {\"type\": \"string\"},\n            \"suggestColor\": {\"type\": \"string\"}\n          }\n        },\n        \"info\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"fgColor\": {\"type\": \"string\"},\n            \"sectionColor\": {\"type\": \"string\"},\n            \"k9sRevColor\": {\"type\": \"string\"},\n            \"cpuColor\": {\"type\": \"string\"},\n            \"memColor\": {\"type\": \"string\"}\n          }\n        },\n        \"help\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"fgColor\": {\"type\": \"string\"},\n            \"bgColor\": {\"type\": \"string\"},\n            \"keyColor\": {\"type\": \"string\"},\n            \"numKeyColor\": {\"type\": \"string\"},\n            \"sectionColor\": {\"type\": \"string\"}\n          }\n        },\n        \"dialog\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"fgColor\": {\"type\": \"string\"},\n            \"bgColor\": {\"type\": \"string\"},\n            \"buttonFgColor\": {\"type\": \"string\"},\n            \"buttonBgColor\": {\"type\": \"string\"},\n            \"buttonFocusFgColor\": {\"type\": \"string\"},\n            \"buttonFocusBgColor\": {\"type\": \"string\"},\n            \"labelFgColor\": {\"type\": \"string\"},\n            \"fieldFgColor\": {\"type\": \"string\"}\n          }\n        },\n        \"frame\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"border\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"fgColor\": {\"type\": \"string\"},\n                \"bgColor\": {\"type\": \"string\"}\n              }\n            },\n            \"menu\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"fgColor\": {\"type\": \"string\"},\n                \"keyColor\": {\"type\": \"string\"},\n                \"numKeyColor\": {\"type\": \"string\"}\n              }\n            },\n            \"crumbs\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"fgColor\": {\"type\": \"string\"},\n                \"keyColor\": {\"type\": \"string\"},\n                \"activeColor\": {\"type\": \"string\"}\n              }\n            },\n            \"status\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"newColor\": {\"type\": \"string\"},\n                \"modifyColor\": {\"type\": \"string\"},\n                \"addColor:\": {\"type\": \"string\"},\n                \"errorColor\": {\"type\": \"string\"},\n                \"highlightColor\": {\"type\": \"string\"},\n                \"killColor\": {\"type\": \"string\"},\n                \"completedColor\": {\"type\": \"string\"}\n              }\n            },\n            \"title\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"fgColor\": {\"type\": \"string\"},\n                \"bgColor\":{\"type\": \"string\"},\n                \"highlightColor\": {\"type\": \"string\"},\n                \"counterColor\":{\"type\": \"string\"},\n                \"filterColor\": {\"type\": \"string\"}\n              }\n            }\n          }\n        },\n        \"views\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"charts\": {\n              \"type\": \"object\",\n              \"properties\": {\n                \"bgColor\": {\"type\": \"string\"},\n                \"defaultDialColors\": {\n                  \"type\": \"array\",\n                  \"items\": {\"type\": \"string\"}\n                },\n                \"defaultChartColors\": {\n                  \"type\": \"array\",\n                  \"items\": {\"type\": \"string\"}\n                }\n              },\n              \"table\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"fgColor\": {\"type\": \"string\"},\n                  \"bgColor\": {\"type\": \"string\"},\n                  \"cursorFgColor\": {\"type\": \"string\"},\n                  \"cursorBgColor\": {\"type\": \"string\"},\n                  \"header\": {\n                    \"type\": \"object\",\n                    \"additionalProperties\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"fgColor\": {\"type\": \"string\"},\n                        \"bgColor\": {\"type\": \"string\"}\n                      }\n                    }\n                  }\n                }\n              },\n              \"xray\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"fgColor\": {\"type\": \"string\"},\n                  \"bgColor\": {\"type\": \"string\"},\n                  \"cursorFgColor\": {\"type\": \"string\"},\n                  \"graphicColor\": {\"type\": \"string\"},\n                  \"showIcons\": {\"type\": \"boolean\"}\n                }\n              },\n              \"yaml\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"keyColor\": {\"type\": \"string\"},\n                  \"colonColor\": {\"type\": \"string\"},\n                  \"valueColor\": {\"type\": \"string\"}\n                }\n              },\n              \"logs\": {\n                \"type\": \"object\",\n                \"properties\": {\n                  \"fgColor\": {\"type\": \"string\"},\n                  \"bgColor\": {\"type\": \"string\"},\n                  \"indicator\": {\n                    \"type\": \"object\",\n                    \"additionalProperties\": {\n                      \"type\": \"object\",\n                      \"properties\": {\n                        \"fgColor\": {\"type\": \"string\"},\n                        \"bgColor\": {\"type\": \"string\"},\n                        \"toggleOnColor\": {\"type\": \"string\"},\n                        \"toggleOffColor\": {\"type\": \"string\"}\n                      }\n                    }\n                  }\n                }\n              }\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "internal/config/json/schemas/views.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"K9s views schema\",\n  \"type\": \"object\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"views\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"type\": \"object\",\n        \"additionalProperties\": false,\n        \"properties\": {\n          \"sortColumn\": { \"type\": \"string\" },\n          \"columns\": {\n            \"type\": \"array\",\n            \"items\": { \"type\": \"string\" }\n          }\n        },\n        \"required\": [\"columns\"]\n      }\n    }\n  },\n  \"required\": [\"views\"]\n}\n"
  },
  {
    "path": "internal/config/json/testdata/aliases/cool.yaml",
    "content": "aliases:\n  blee: duh\n  fred: zorg\n"
  },
  {
    "path": "internal/config/json/testdata/aliases/toast.yaml",
    "content": "alias:\n  blee: duh\n  fred: zorg\n"
  },
  {
    "path": "internal/config/json/testdata/context/cool.yaml",
    "content": "k9s:\n  cluster: kind-dashb\n  readOnly: false\n  skin: nightfox\n  namespace:\n    active: default\n    lockFavorites: false\n    favorites:\n    - kube-system\n    - default\n  view:\n    active: pod\n  featureGates:\n    nodeShell: false\n"
  },
  {
    "path": "internal/config/json/testdata/context/toast.yaml",
    "content": "k9s:\n  cluster: kind-dashb\n  readOnly: false\n  skin: nightfox\n  namespaces:\n    active: default\n    lockFavorites: false\n    favorites:\n    - kube-system\n    - default\n  view:\n    active: pod\n  fred: blee\n  featureGates:\n    nodeShell: false\n"
  },
  {
    "path": "internal/config/json/testdata/hotkeys/cool.yaml",
    "content": "hotKey:\n  shift-0:\n    shortCut:    Shift-0\n    description: Popeye\n    command:     popeye\n  shift-1:\n    shortCut:    Shift-1\n    description: View deployments\n    command:     dp\n  shift-2:\n    shortCut:    Shift-2\n    description: View services\n    command:     service\n  shift-3:\n    shortCut:    Shift-3\n    description: View statefulsets\n    command:     sts\n  shift-4:\n    shortCut:    Shift-4\n    description: Xray Deployments\n    command:     xray dp\n  shift-5:\n    shortCut:    Shift-5\n    description: Xray StatefulSets\n    command:     xray sts\n  shift-6:\n    shortCut:    Shift-6\n    description: Xray DaemonSets\n    command:     xray ds\n  shift-7:\n    shortCut:    Shift-7\n    description: Xray Services\n    command:     xray svc\n"
  },
  {
    "path": "internal/config/json/testdata/k9s/cool.yaml",
    "content": "k9s:\n  liveViewAutoRefresh: false\n  screenDumpDir: /Users/fernand/.local/state/k9s/screen-dumps\n  refreshRate: 2\n  maxConnRetry: 5\n  readOnly: false\n  noExitOnCtrlC: false\n  ui:\n    enableMouse: false\n    headless: false\n    logoless: false\n    crumbsless: false\n    splashless: false\n    noIcons: false\n  skipLatestRevCheck: false\n  disablePodCounting: false\n  shellPod:\n    image: busybox:1.37.0\n    namespace: default\n    limits:\n      cpu: 100m\n      memory: 100Mi\n  imageScans:\n    enable: false\n    exclusions:\n      namespaces: []\n      labels: {}\n  logger:\n    tail: 100\n    buffer: 5000\n    sinceSeconds: -1\n    textWrap: false\n    disableAutoscroll: false\n    columnLock: false\n    showTime: false\n  thresholds:\n    cpu:\n      critical: 90\n      warn: 70\n    memory:\n      critical: 90\n      warn: 70\n"
  },
  {
    "path": "internal/config/json/testdata/k9s/toast.yaml",
    "content": "k9s:\n  liveViewAutoRefresh: false\n  screenDumpDir: /Users/fernand/.local/state/k9s/screen-dumps\n  refreshRate: 2\n  maxConnRetry: 5\n  readOnly: false\n  noExitOnCtrlC: false\n  skipLatestRevCheck: false\n  disablePodCounting: false\n  shellPods:\n    image: busybox:1.37.0\n    namespace: default\n    limits:\n      cpu: 100m\n      memory: 100Mi\n  imageScans:\n    enable: false\n    exclusions:\n      namespaces: []\n      labels: {}\n  logger:\n    tail: 100\n    buffer: 5000\n    sinceSeconds: -1\n    textWrap: false\n    disableAutoscroll: false\n    columnLock: false\n    showTime: false\n  thresholds:\n    cpu:\n      critical: 90\n      warn: 70\n    memory:\n      critical: 90\n      warn: 70\n"
  },
  {
    "path": "internal/config/json/testdata/plugins/cool.yaml",
    "content": "plugins:\n  blee:\n    shortCut: g\n    confirm: false\n    description: blee\n    scopes:\n      - namespaces\n    command: sh\n    background: false\n    args:\n      - -c\n      - \"blee bla\"\n  duh:\n    shortCut: h\n    confirm: true\n    description: duh\n    scopes:\n      - all\n    command: sh\n    background: true\n    args:\n      - -c\n      - \"duh fred\"\n"
  },
  {
    "path": "internal/config/json/testdata/plugins/snippet.yaml",
    "content": "shortCut: g\nconfirm: false\ndescription: blee\nscopes:\n  - namespaces\ncommand: sh\nbackground: false\nargs:\n  - -c\n  - \"blee bla\"\n"
  },
  {
    "path": "internal/config/json/testdata/plugins/snippets.yaml",
    "content": "blee:\n  shortCut: g\n  confirm: false\n  description: blee\n  scopes:\n    - namespaces\n  command: sh\n  background: false\n  args:\n    - -c\n    - \"blee bla\"\n\nduh:\n  shortCut: h\n  confirm: true\n  description: duh\n  scopes:\n    - all\n  command: sh\n  background: true\n  args:\n    - -c\n    - \"duh fred\""
  },
  {
    "path": "internal/config/json/testdata/plugins/toast.yaml",
    "content": "plugins:\n  blee:\n    shortCuts: g\n    confirm: false\n    description: blee\n    scopes:\n      - namespaces\n    command: sh\n    background: false\n    args:\n      - -c\n      - \"blee bla\"\n  duh:\n    shortCut: h\n    confirm: true\n    description: duh\n    command: sh\n    background: true\n    args:\n      - -c\n      - \"duh fred\"\n"
  },
  {
    "path": "internal/config/json/testdata/skins/cool.yaml",
    "content": "# -----------------------------------------------------------------------------\n# K9s Nightfox Theme\n# Based on the Nightfox.nvim color scheme:\n# https://github.com/EdenEast/nightfox.nvim\n# -----------------------------------------------------------------------------\n\n# Styles...\nforeground: &foreground \"#cdcecf\"\nbackground: &background \"#192330\"\ncurrent_line: &current_line \"#2b3b51\"\nselection: &selection \"#2b3b51\"\ncomment: &comment \"#738091\"\ncyan: &cyan \"#63cdcf\"\ngreen: &green \"#81b29a\"\norange: &orange \"#f4a261\"\nmagenta: &magenta \"#9d79d6\"\nblue: &blue \"#719cd6\"\nred: &red \"#c94f6d\"\n\n# Skin...\nk9s:\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *blue\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *orange\n  info:\n    fgColor: *magenta\n    sectionColor: *foreground\n  help:\n    fgColor: *foreground\n    bgColor: *background\n    keyColor: *magenta\n    numKeyColor: *magenta\n    sectionColor: *foreground\n  dialog:\n    fgColor: *foreground\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *magenta\n    buttonFocusFgColor: white\n    buttonFocusBgColor: *cyan\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  frame:\n    border:\n      fgColor: *selection\n      focusColor: *current_line\n    menu:\n      fgColor: *foreground\n      keyColor: *magenta\n      numKeyColor: *magenta\n    crumbs:\n      fgColor: *foreground\n      bgColor: *current_line\n      activeColor: *current_line\n    status:\n      newColor: *cyan\n      modifyColor: *blue\n      addColor: *green\n      errorColor: *red\n      highlightColor: *orange\n      killColor: *comment\n      completedColor: *comment\n    title:\n      fgColor: *foreground\n      bgColor: *current_line\n      highlightColor: *orange\n      counterColor: *blue\n      filterColor: *magenta\n  views:\n    charts:\n      bgColor: default\n      defaultDialColors:\n        - *blue\n        - *red\n      defaultChartColors:\n        - *blue\n        - *red\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      cursorFgColor: *selection\n      cursorBgColor: *current_line\n      header:\n        fgColor: *foreground\n        bgColor: *background\n        sorterColor: *cyan\n    xray:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *current_line\n      graphicColor: *blue\n      showIcons: false\n    yaml:\n      keyColor: *magenta\n      colonColor: *blue\n      valueColor: *foreground\n    logs:\n      fgColor: *foreground\n      bgColor: *background\n      indicator:\n        fgColor: *foreground\n        bgColor: *selection\n        toggleOnColor: *magenta\n        toggleOffColor: *blue\n"
  },
  {
    "path": "internal/config/json/testdata/skins/toast.yaml",
    "content": "# -----------------------------------------------------------------------------\n# K9s Nightfox Theme\n# Based on the Nightfox.nvim color scheme:\n# https://github.com/EdenEast/nightfox.nvim\n# -----------------------------------------------------------------------------\n\n# Styles...\nforeground: &foreground \"#cdcecf\"\nbackground: &background \"#192330\"\ncurrent_line: &current_line \"#2b3b51\"\nselection: &selection \"#2b3b51\"\ncomment: &comment \"#738091\"\ncyan: &cyan \"#63cdcf\"\ngreen: &green \"#81b29a\"\norange: &orange \"#f4a261\"\nmagenta: &magenta \"#9d79d6\"\nblue: &blue \"#719cd6\"\nred: &red \"#c94f6d\"\n\n# Skin...\nk9s:\n  bodys:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *blue\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *orange\n  info:\n    fgColor: *magenta\n    sectionColor: *foreground\n  dialog:\n    fgColor: *foreground\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *magenta\n    buttonFocusFgColor: white\n    buttonFocusBgColor: *cyan\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  frame:\n    border:\n      fgColor: *selection\n      focusColor: *current_line\n    menu:\n      fgColor: *foreground\n      keyColor: *magenta\n      numKeyColor: *magenta\n    crumbs:\n      fgColor: *foreground\n      bgColor: *current_line\n      activeColor: *current_line\n    status:\n      newColor: *cyan\n      modifyColor: *blue\n      addColor: *green\n      errorColor: *red\n      highlightColor: *orange\n      killColor: *comment\n      completedColor: *comment\n    title:\n      fgColor: *foreground\n      bgColor: *current_line\n      highlightColor: *orange\n      counterColor: *blue\n      filterColor: *magenta\n  views:\n    charts:\n      bgColor: default\n      defaultDialColors:\n        - *blue\n        - *red\n      defaultChartColors:\n        - *blue\n        - *red\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      cursorFgColor: *selection\n      cursorBgColor: *current_line\n      header:\n        fgColor: *foreground\n        bgColor: *background\n        sorterColor: *cyan\n    xray:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *current_line\n      graphicColor: *blue\n      showIcons: false\n    yaml:\n      keyColor: *magenta\n      colonColor: *blue\n      valueColor: *foreground\n    logs:\n      fgColor: *foreground\n      bgColor: *background\n      indicator:\n        fgColor: *foreground\n        bgColor: *selection\n        toggleOnColor: *magenta\n        toggleOffColor: *blue\n"
  },
  {
    "path": "internal/config/json/testdata/views/cool.yaml",
    "content": "views:\n  v1/nodes:\n    columns:\n      - NAME\n      - IP\n  v1/endpoints:\n    sortColumn: AGE:asc\n    columns:\n      - NAME\n      - NAMESPACE\n      - ENDPOINTS\n      - AGE\n"
  },
  {
    "path": "internal/config/json/testdata/views/toast.yaml",
    "content": "views:\n  v1/nodes:\n  v1/endpoints:\n    sortCol: AGE:asc\n    cols:\n      - NAME\n      - NAMESPACE\n      - ENDPOINTS\n      - AGE\n"
  },
  {
    "path": "internal/config/json/validator.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage json\n\nimport (\n\t\"cmp\"\n\t_ \"embed\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"slices\"\n\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/xeipuuv/gojsonschema\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nconst (\n\t// PluginsSchema describes plugins schema.\n\tPluginsSchema = \"plugins.json\"\n\n\t// PluginSchema describes a plugin snippet schema.\n\tPluginSchema = \"plugin.json\"\n\n\t// PluginMultiSchema describes plugin snippets schema.\n\tPluginMultiSchema = \"plugin-multi.json\"\n\n\t// AliasesSchema describes aliases schema.\n\tAliasesSchema = \"aliases.json\"\n\n\t// ViewsSchema describes views schema.\n\tViewsSchema = \"views.json\"\n\n\t// HotkeysSchema describes hotkeys schema.\n\tHotkeysSchema = \"hotkeys.json\"\n\n\t// K9sSchema describes k9s config schema.\n\tK9sSchema = \"k9s.json\"\n\n\t// ContextSchema describes context config schema.\n\tContextSchema = \"context.json\"\n\n\t// SkinSchema describes skin config schema.\n\tSkinSchema = \"skin.json\"\n)\n\nvar (\n\t//go:embed schemas/plugins.json\n\tpluginsSchema string\n\n\t//go:embed schemas/plugin.json\n\tpluginSchema string\n\n\t//go:embed schemas/plugin-multi.json\n\tpluginMultiSchema string\n\n\t//go:embed schemas/aliases.json\n\taliasSchema string\n\n\t//go:embed schemas/views.json\n\tviewsSchema string\n\n\t//go:embed schemas/k9s.json\n\tk9sSchema string\n\n\t//go:embed schemas/context.json\n\tcontextSchema string\n\n\t//go:embed schemas/hotkeys.json\n\thotkeysSchema string\n\n\t//go:embed schemas/skin.json\n\tskinSchema string\n)\n\n// Validator tracks schemas validation.\ntype Validator struct {\n\tschemas map[string]gojsonschema.JSONLoader\n\tloader  *gojsonschema.SchemaLoader\n}\n\n// NewValidator returns a new instance.\nfunc NewValidator() *Validator {\n\tv := Validator{\n\t\tschemas: map[string]gojsonschema.JSONLoader{\n\t\t\tK9sSchema:         gojsonschema.NewStringLoader(k9sSchema),\n\t\t\tContextSchema:     gojsonschema.NewStringLoader(contextSchema),\n\t\t\tAliasesSchema:     gojsonschema.NewStringLoader(aliasSchema),\n\t\t\tViewsSchema:       gojsonschema.NewStringLoader(viewsSchema),\n\t\t\tPluginsSchema:     gojsonschema.NewStringLoader(pluginsSchema),\n\t\t\tPluginSchema:      gojsonschema.NewStringLoader(pluginSchema),\n\t\t\tPluginMultiSchema: gojsonschema.NewStringLoader(pluginMultiSchema),\n\t\t\tHotkeysSchema:     gojsonschema.NewStringLoader(hotkeysSchema),\n\t\t\tSkinSchema:        gojsonschema.NewStringLoader(skinSchema),\n\t\t},\n\t}\n\tv.register()\n\n\treturn &v\n}\n\n// Init initializes the schemas.\nfunc (v *Validator) register() {\n\tv.loader = gojsonschema.NewSchemaLoader()\n\tv.loader.Validate = true\n\n\tclog := slog.With(slogs.Subsys, \"schema\")\n\tfor k, s := range v.schemas {\n\t\tif err := v.loader.AddSchema(k, s); err != nil {\n\t\t\tclog.Error(\"Schema initialization failed\",\n\t\t\t\tslogs.SchemaFile, k,\n\t\t\t\tslogs.Error, err,\n\t\t\t)\n\t\t}\n\t}\n}\n\n// ValidatePlugins validates plugins schema.\n// Checks for full, snippet and multi snippets schemas.\nfunc (v *Validator) ValidatePlugins(bb []byte) (string, error) {\n\tvar errs error\n\tfor _, k := range []string{PluginsSchema, PluginSchema, PluginMultiSchema} {\n\t\tif err := v.Validate(k, bb); err != nil {\n\t\t\terrs = errors.Join(errs, err)\n\t\t\tcontinue\n\t\t}\n\t\treturn k, nil\n\t}\n\n\treturn \"\", errs\n}\n\n// Validate runs document thru given schema validation.\nfunc (v *Validator) Validate(k string, bb []byte) error {\n\tvar m any\n\terr := yaml.Unmarshal(bb, &m)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ts, ok := v.schemas[k]\n\tif !ok {\n\t\treturn fmt.Errorf(\"no schema found for: %q\", k)\n\t}\n\tresult, err := gojsonschema.Validate(s, gojsonschema.NewGoLoader(m))\n\tif err != nil {\n\t\treturn err\n\t}\n\tif result.Valid() {\n\t\treturn nil\n\t}\n\n\tslices.SortFunc(result.Errors(), func(a, b gojsonschema.ResultError) int {\n\t\treturn cmp.Compare(a.Description(), b.Description())\n\t})\n\tvar errs error\n\tfor _, re := range result.Errors() {\n\t\terrs = errors.Join(errs, errors.New(re.Description()))\n\t}\n\n\treturn errs\n}\n\nfunc (v *Validator) ValidateObj(k string, o any) error {\n\ts, ok := v.schemas[k]\n\tif !ok {\n\t\treturn fmt.Errorf(\"no schema found for: %q\", k)\n\t}\n\tresult, err := gojsonschema.Validate(s, gojsonschema.NewGoLoader(o))\n\tif err != nil {\n\t\treturn err\n\t}\n\tif result.Valid() {\n\t\treturn nil\n\t}\n\n\tslices.SortFunc(result.Errors(), func(a, b gojsonschema.ResultError) int {\n\t\treturn cmp.Compare(a.Description(), b.Description())\n\t})\n\tvar errs error\n\tfor _, re := range result.Errors() {\n\t\terrs = errors.Join(errs, errors.New(re.Description()))\n\t}\n\n\treturn errs\n}\n"
  },
  {
    "path": "internal/config/json/validator_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage json_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config/json\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestValidatePluginSnippet(t *testing.T) {\n\tplugPath := \"testdata/plugins/snippet.yaml\"\n\tbb, err := os.ReadFile(plugPath)\n\trequire.NoError(t, err)\n\n\tp := json.NewValidator()\n\trequire.NoError(t, p.Validate(json.PluginSchema, bb), plugPath)\n}\n\nfunc TestValidatePlugins(t *testing.T) {\n\tuu := map[string]struct {\n\t\tpath, schema string\n\t\terr          string\n\t}{\n\t\t\"cool\": {\n\t\t\tpath:   \"testdata/plugins/cool.yaml\",\n\t\t\tschema: json.PluginsSchema,\n\t\t},\n\t\t\"toast\": {\n\t\t\tpath:   \"testdata/plugins/toast.yaml\",\n\t\t\tschema: json.PluginsSchema,\n\t\t\terr:    \"scopes is required\\nshortCut is required\",\n\t\t},\n\t\t\"cool-snippet\": {\n\t\t\tpath:   \"testdata/plugins/snippet.yaml\",\n\t\t\tschema: json.PluginSchema,\n\t\t},\n\t\t\"cool-snippets\": {\n\t\t\tpath:   \"testdata/plugins/snippets.yaml\",\n\t\t\tschema: json.PluginMultiSchema,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tbb, err := os.ReadFile(u.path)\n\t\t\trequire.NoError(t, err)\n\t\t\tv := json.NewValidator()\n\t\t\tif err := v.Validate(u.schema, bb); err != nil {\n\t\t\t\tassert.Equal(t, u.err, err.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidatePluginDir(t *testing.T) {\n\tplugDir := \"../../../plugins\"\n\tee, err := os.ReadDir(plugDir)\n\trequire.NoError(t, err)\n\tfor _, e := range ee {\n\t\tif e.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\text := filepath.Ext(e.Name())\n\t\tif ext == \".md\" {\n\t\t\tcontinue\n\t\t}\n\t\tassert.Equal(t, \".yaml\", ext, \"expected yaml file: %q\", e.Name())\n\t\tassert.NotContains(t, \"_\", e.Name(), \"underscore in: %q\", e.Name())\n\t\tbb, err := os.ReadFile(filepath.Join(plugDir, e.Name()))\n\t\trequire.NoError(t, err)\n\n\t\tp := json.NewValidator()\n\t\trequire.NoError(t, p.Validate(json.PluginsSchema, bb), e.Name())\n\t}\n}\n\nfunc TestValidateSkinDir(t *testing.T) {\n\tskinDir := \"../../../skins\"\n\tee, err := os.ReadDir(skinDir)\n\trequire.NoError(t, err)\n\tp := json.NewValidator()\n\tfor _, e := range ee {\n\t\tif e.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\text := filepath.Ext(e.Name())\n\t\tassert.Equal(t, \".yaml\", ext, \"expected yaml file: %q\", e.Name())\n\t\tassert.NotContains(t, \"_\", e.Name(), \"underscore in: %q\", e.Name())\n\t\tbb, err := os.ReadFile(filepath.Join(skinDir, e.Name()))\n\t\trequire.NoError(t, err)\n\t\trequire.NoError(t, p.Validate(json.SkinSchema, bb), e.Name())\n\t}\n}\n\nfunc TestValidateSkin(t *testing.T) {\n\tuu := map[string]struct {\n\t\tf   string\n\t\terr string\n\t}{\n\t\t\"happy\": {\n\t\t\tf: \"testdata/skins/cool.yaml\",\n\t\t},\n\t\t\"toast\": {\n\t\t\tf:   \"testdata/skins/toast.yaml\",\n\t\t\terr: `Additional property bodys is not allowed`,\n\t\t},\n\t}\n\n\tv := json.NewValidator()\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tbb, err := os.ReadFile(u.f)\n\t\t\trequire.NoError(t, err)\n\t\t\tif err := v.Validate(json.SkinSchema, bb); err != nil {\n\t\t\t\tassert.Equal(t, u.err, err.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidateK9s(t *testing.T) {\n\tuu := map[string]struct {\n\t\tf   string\n\t\terr string\n\t}{\n\t\t\"happy\": {\n\t\t\tf: \"testdata/k9s/cool.yaml\",\n\t\t},\n\t\t\"toast\": {\n\t\t\tf:   \"testdata/k9s/toast.yaml\",\n\t\t\terr: `Additional property shellPods is not allowed`,\n\t\t},\n\t}\n\n\tv := json.NewValidator()\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tbb, err := os.ReadFile(u.f)\n\t\t\trequire.NoError(t, err)\n\t\t\tif err := v.Validate(json.K9sSchema, bb); err != nil {\n\t\t\t\tassert.Equal(t, u.err, err.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidateContext(t *testing.T) {\n\tuu := map[string]struct {\n\t\tf   string\n\t\terr string\n\t}{\n\t\t\"happy\": {\n\t\t\tf: \"testdata/context/cool.yaml\",\n\t\t},\n\t\t\"toast\": {\n\t\t\tf: \"testdata/context/toast.yaml\",\n\t\t\terr: `Additional property fred is not allowed\nAdditional property namespaces is not allowed`,\n\t\t},\n\t}\n\n\tv := json.NewValidator()\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tbb, err := os.ReadFile(u.f)\n\t\t\trequire.NoError(t, err)\n\t\t\tif err := v.Validate(json.ContextSchema, bb); err != nil {\n\t\t\t\tassert.Equal(t, u.err, err.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidateAliases(t *testing.T) {\n\tuu := map[string]struct {\n\t\tf   string\n\t\terr string\n\t}{\n\t\t\"happy\": {\n\t\t\tf: \"testdata/aliases/cool.yaml\",\n\t\t},\n\t\t\"toast\": {\n\t\t\tf: \"testdata/aliases/toast.yaml\",\n\t\t\terr: `Additional property alias is not allowed\naliases is required`,\n\t\t},\n\t}\n\n\tv := json.NewValidator()\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tbb, err := os.ReadFile(u.f)\n\t\t\trequire.NoError(t, err)\n\t\t\tif err := v.Validate(json.AliasesSchema, bb); err != nil {\n\t\t\t\tassert.Equal(t, u.err, err.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestValidateViews(t *testing.T) {\n\tuu := map[string]struct {\n\t\tf   string\n\t\terr string\n\t}{\n\t\t\"happy\": {\n\t\t\tf: \"testdata/views/cool.yaml\",\n\t\t},\n\t\t\"toast\": {\n\t\t\tf: \"testdata/views/toast.yaml\",\n\t\t\terr: `Additional property cols is not allowed\nAdditional property sortCol is not allowed\nInvalid type. Expected: object, given: null\ncolumns is required`,\n\t\t},\n\t}\n\n\tv := json.NewValidator()\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tbb, err := os.ReadFile(u.f)\n\t\t\trequire.NoError(t, err)\n\t\t\tif err := v.Validate(json.ViewsSchema, bb); err != nil {\n\t\t\t\tassert.Equal(t, u.err, err.Error())\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/config/k9s.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n)\n\ntype gpuVendors map[string]string\n\n// KnownGPUVendors tracks a set of known GPU vendors.\nvar KnownGPUVendors = defaultGPUVendors\n\nvar defaultGPUVendors = gpuVendors{\n\t\"nvidia\":        \"nvidia.com/gpu\",\n\t\"nvidia-shared\": \"nvidia.com/gpu.shared\",\n\t\"amd\":           \"amd.com/gpu\",\n\t\"intel\":         \"gpu.intel.com/i915\",\n}\n\n// K9s tracks K9s configuration options.\ntype K9s struct {\n\tLiveViewAutoRefresh bool       `json:\"liveViewAutoRefresh\" yaml:\"liveViewAutoRefresh\"`\n\tGPUVendors          gpuVendors `json:\"gpuVendors\" yaml:\"gpuVendors\"`\n\tScreenDumpDir       string     `json:\"screenDumpDir\" yaml:\"screenDumpDir,omitempty\"`\n\tRefreshRate         float32    `json:\"refreshRate\" yaml:\"refreshRate\"`\n\tAPIServerTimeout    string     `json:\"apiServerTimeout\" yaml:\"apiServerTimeout\"`\n\tMaxConnRetry        int32      `json:\"maxConnRetry\" yaml:\"maxConnRetry\"`\n\tReadOnly            bool       `json:\"readOnly\" yaml:\"readOnly\"`\n\tNoExitOnCtrlC       bool       `json:\"noExitOnCtrlC\" yaml:\"noExitOnCtrlC\"`\n\tPortForwardAddress  string     `yaml:\"portForwardAddress\"`\n\tUI                  UI         `json:\"ui\" yaml:\"ui\"`\n\tSkipLatestRevCheck  bool       `json:\"skipLatestRevCheck\" yaml:\"skipLatestRevCheck\"`\n\tDisablePodCounting  bool       `json:\"disablePodCounting\" yaml:\"disablePodCounting\"`\n\tShellPod            *ShellPod  `json:\"shellPod\" yaml:\"shellPod\"`\n\tImageScans          ImageScans `json:\"imageScans\" yaml:\"imageScans\"`\n\tLogger              Logger     `json:\"logger\" yaml:\"logger\"`\n\tThresholds          Threshold  `json:\"thresholds\" yaml:\"thresholds\"`\n\tDefaultView         string     `json:\"defaultView\" yaml:\"defaultView\"`\n\tmanualRefreshRate   float32\n\tmanualReadOnly      *bool\n\tmanualCommand       *string\n\tmanualScreenDumpDir *string\n\trefreshRateWarned   bool\n\tdir                 *data.Dir\n\tactiveContextName   string\n\tactiveConfig        *data.Config\n\tconn                client.Connection\n\tks                  data.KubeSettings\n\tmx                  sync.RWMutex\n\tcontextSwitch       bool\n}\n\n// NewK9s create a new K9s configuration.\nfunc NewK9s(conn client.Connection, ks data.KubeSettings) *K9s {\n\treturn &K9s{\n\t\tRefreshRate:        defaultRefreshRate,\n\t\tGPUVendors:         make(gpuVendors),\n\t\tMaxConnRetry:       defaultMaxConnRetry,\n\t\tAPIServerTimeout:   client.DefaultCallTimeoutDuration.String(),\n\t\tScreenDumpDir:      AppDumpsDir,\n\t\tLogger:             NewLogger(),\n\t\tThresholds:         NewThreshold(),\n\t\tPortForwardAddress: defaultPFAddress(),\n\t\tShellPod:           NewShellPod(),\n\t\tImageScans:         NewImageScans(),\n\t\tdir:                data.NewDir(AppContextsDir),\n\t\tconn:               conn,\n\t\tks:                 ks,\n\t}\n}\n\nfunc (k *K9s) ToggleContextSwitch(b bool) {\n\tk.mx.Lock()\n\tdefer k.mx.Unlock()\n\n\tk.contextSwitch = b\n}\n\nfunc (k *K9s) getContextSwitch() bool {\n\tk.mx.Lock()\n\tdefer k.mx.Unlock()\n\n\treturn k.contextSwitch\n}\n\nfunc (k *K9s) resetConnection(conn client.Connection) {\n\tk.mx.Lock()\n\tdefer k.mx.Unlock()\n\n\tk.conn = conn\n}\n\n// Save saves the k9s config to disk.\nfunc (k *K9s) Save(contextName, clusterName string, force bool) error {\n\tpath := filepath.Join(\n\t\tAppContextsDir,\n\t\tdata.SanitizeContextSubpath(clusterName, contextName),\n\t\tdata.MainConfigFile,\n\t)\n\n\tif _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) || force {\n\t\tslog.Debug(\"[CONFIG] Saving context config to disk\",\n\t\t\tslogs.Path, path,\n\t\t\tslogs.Cluster, k.getActiveConfig().Context.GetClusterName(),\n\t\t\tslogs.Context, k.getActiveContextName(),\n\t\t)\n\t\treturn k.dir.Save(path, k.getActiveConfig())\n\t}\n\n\treturn nil\n}\n\n// Merge merges k9s configs.\nfunc (k *K9s) Merge(k1 *K9s) {\n\tif k1 == nil {\n\t\treturn\n\t}\n\n\tfor k, v := range k1.GPUVendors {\n\t\tKnownGPUVendors[k] = v\n\t}\n\n\tk.LiveViewAutoRefresh = k1.LiveViewAutoRefresh\n\tk.DefaultView = k1.DefaultView\n\tk.ScreenDumpDir = k1.ScreenDumpDir\n\tk.RefreshRate = k1.RefreshRate\n\tk.APIServerTimeout = k1.APIServerTimeout\n\tk.MaxConnRetry = k1.MaxConnRetry\n\tk.ReadOnly = k1.ReadOnly\n\tk.NoExitOnCtrlC = k1.NoExitOnCtrlC\n\tk.PortForwardAddress = k1.PortForwardAddress\n\tk.UI = k1.UI\n\tk.SkipLatestRevCheck = k1.SkipLatestRevCheck\n\tk.DisablePodCounting = k1.DisablePodCounting\n\tk.ShellPod = k1.ShellPod\n\tk.Logger = k1.Logger\n\tk.ImageScans = k1.ImageScans\n\tif k1.Thresholds != nil {\n\t\tk.Thresholds = k1.Thresholds\n\t}\n}\n\n// AppScreenDumpDir fetch screen dumps dir.\nfunc (k *K9s) AppScreenDumpDir() string {\n\td := k.ScreenDumpDir\n\tif isStringSet(k.manualScreenDumpDir) {\n\t\td = *k.manualScreenDumpDir\n\t\tk.ScreenDumpDir = d\n\t}\n\tif d == \"\" {\n\t\td = AppDumpsDir\n\t}\n\n\treturn d\n}\n\n// ContextScreenDumpDir fetch context specific screen dumps dir.\nfunc (k *K9s) ContextScreenDumpDir() string {\n\treturn filepath.Join(k.AppScreenDumpDir(), k.contextPath())\n}\n\nfunc (k *K9s) contextPath() string {\n\tif k.getActiveConfig() == nil {\n\t\treturn \"na\"\n\t}\n\n\treturn data.SanitizeContextSubpath(\n\t\tk.getActiveConfig().Context.GetClusterName(),\n\t\tk.ActiveContextName(),\n\t)\n}\n\n// Reset resets configuration and context.\nfunc (k *K9s) Reset() {\n\tk.setActiveConfig(nil)\n\tk.setActiveContextName(\"\")\n}\n\n// ActiveContextNamespace fetch the context active ns.\nfunc (k *K9s) ActiveContextNamespace() (string, error) {\n\tact, err := k.ActiveContext()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn act.Namespace.Active, nil\n}\n\n// ActiveContextName returns the active context name.\nfunc (k *K9s) ActiveContextName() string {\n\treturn k.getActiveContextName()\n}\n\n// ActiveContext returns the currently active context.\nfunc (k *K9s) ActiveContext() (*data.Context, error) {\n\tif cfg := k.getActiveConfig(); cfg != nil && cfg.Context != nil {\n\t\treturn cfg.Context, nil\n\t}\n\tct, err := k.ActivateContext(k.ActiveContextName())\n\n\treturn ct, err\n}\n\nfunc (k *K9s) setActiveConfig(c *data.Config) {\n\tk.mx.Lock()\n\tdefer k.mx.Unlock()\n\n\tk.activeConfig = c\n}\n\nfunc (k *K9s) getActiveConfig() *data.Config {\n\tk.mx.RLock()\n\tdefer k.mx.RUnlock()\n\n\treturn k.activeConfig\n}\n\nfunc (k *K9s) setActiveContextName(n string) {\n\tk.mx.Lock()\n\tdefer k.mx.Unlock()\n\n\tk.activeContextName = n\n}\n\nfunc (k *K9s) getActiveContextName() string {\n\tk.mx.RLock()\n\tdefer k.mx.RUnlock()\n\n\treturn k.activeContextName\n}\n\n// ActivateContext initializes the active context if not present.\nfunc (k *K9s) ActivateContext(contextName string) (*data.Context, error) {\n\tk.setActiveContextName(contextName)\n\tct, err := k.ks.GetContext(contextName)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcfg, err := k.dir.Load(contextName, ct)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tk.setActiveConfig(cfg)\n\n\tif cfg.Context.Proxy != nil {\n\t\tk.ks.SetProxy(func(*http.Request) (*url.URL, error) {\n\t\t\tslog.Debug(\"Using proxy address\", slogs.Address, cfg.Context.Proxy.Address)\n\t\t\treturn url.Parse(cfg.Context.Proxy.Address)\n\t\t})\n\n\t\tif k.conn != nil && k.conn.Config() != nil {\n\t\t\t// We get on this branch when the user switches the context and k9s\n\t\t\t// already has an API connection object so we just set the proxy to\n\t\t\t// avoid recreation using client.InitConnection\n\t\t\tk.conn.Config().SetProxy(func(*http.Request) (*url.URL, error) {\n\t\t\t\tslog.Debug(\"Setting proxy address\", slogs.Address, cfg.Context.Proxy.Address)\n\t\t\t\treturn url.Parse(cfg.Context.Proxy.Address)\n\t\t\t})\n\n\t\t\tif !k.conn.CheckConnectivity() {\n\t\t\t\treturn nil, fmt.Errorf(\"unable to connect to context %q\", contextName)\n\t\t\t}\n\t\t}\n\t}\n\n\tk.Validate(k.conn, contextName, ct.Cluster)\n\t// If the context specifies a namespace, use it!\n\tif ns := ct.Namespace; ns != client.BlankNamespace {\n\t\tk.getActiveConfig().Context.Namespace.Active = ns\n\t} else if k.getActiveConfig().Context.Namespace.Active == \"\" {\n\t\tk.getActiveConfig().Context.Namespace.Active = client.DefaultNamespace\n\t}\n\tif k.getActiveConfig().Context == nil {\n\t\treturn nil, fmt.Errorf(\"context activation failed for: %s\", contextName)\n\t}\n\n\treturn k.getActiveConfig().Context, nil\n}\n\n// Reload reloads the context config from disk.\nfunc (k *K9s) Reload() error {\n\t// Switching context skipping reload...\n\tif k.getContextSwitch() {\n\t\treturn nil\n\t}\n\tct, err := k.ks.GetContext(k.getActiveContextName())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcfg, err := k.dir.Load(k.getActiveContextName(), ct)\n\tif err != nil {\n\t\treturn err\n\t}\n\tk.setActiveConfig(cfg)\n\tk.getActiveConfig().Validate(k.conn, k.getActiveContextName(), ct.Cluster)\n\n\treturn nil\n}\n\n// Override overrides k9s config from cli args.\nfunc (k *K9s) Override(k9sFlags *Flags) {\n\tif k9sFlags.RefreshRate != nil && *k9sFlags.RefreshRate != DefaultRefreshRate {\n\t\tk.manualRefreshRate = float32(*k9sFlags.RefreshRate)\n\t}\n\n\tk.UI.manualHeadless = k9sFlags.Headless\n\tk.UI.manualLogoless = k9sFlags.Logoless\n\tk.UI.manualCrumbsless = k9sFlags.Crumbsless\n\tk.UI.manualSplashless = k9sFlags.Splashless\n\tk.UI.manualInvert = k9sFlags.Invert\n\tif k9sFlags.ReadOnly != nil && *k9sFlags.ReadOnly {\n\t\tk.manualReadOnly = k9sFlags.ReadOnly\n\t}\n\tif k9sFlags.Write != nil && *k9sFlags.Write {\n\t\tvar falseVal bool\n\t\tk.manualReadOnly = &falseVal\n\t}\n\tk.manualCommand = k9sFlags.Command\n\tk.manualScreenDumpDir = k9sFlags.ScreenDumpDir\n}\n\n// IsHeadless returns headless setting.\nfunc (k *K9s) IsHeadless() bool {\n\tif IsBoolSet(k.UI.manualHeadless) {\n\t\treturn true\n\t}\n\n\treturn k.UI.Headless\n}\n\n// IsLogoless returns logoless setting.\nfunc (k *K9s) IsLogoless() bool {\n\tif IsBoolSet(k.UI.manualLogoless) {\n\t\treturn true\n\t}\n\n\treturn k.UI.Logoless\n}\n\n// IsCrumbsless returns crumbsless setting.\nfunc (k *K9s) IsCrumbsless() bool {\n\tif IsBoolSet(k.UI.manualCrumbsless) {\n\t\treturn true\n\t}\n\n\treturn k.UI.Crumbsless\n}\n\n// IsSplashless returns splashless setting.\nfunc (k *K9s) IsSplashless() bool {\n\tif IsBoolSet(k.UI.manualSplashless) {\n\t\treturn true\n\t}\n\n\treturn k.UI.Splashless\n}\n\n// IsInvert returns invert setting.\nfunc (k *K9s) IsInvert() bool {\n\tif IsBoolSet(k.UI.manualInvert) {\n\t\treturn true\n\t}\n\n\treturn k.UI.Invert\n}\n\n// GetRefreshRate returns the current refresh rate.\nfunc (k *K9s) GetRefreshRate() float32 {\n\tk.mx.Lock()\n\tdefer k.mx.Unlock()\n\n\trate := k.RefreshRate\n\tif k.manualRefreshRate != 0 {\n\t\trate = k.manualRefreshRate\n\t}\n\tif rate < DefaultRefreshRate {\n\t\tif !k.refreshRateWarned {\n\t\t\tslog.Warn(\"Refresh rate is below minimum, capping to minimum value\",\n\t\t\t\tslogs.Requested, float64(rate),\n\t\t\t\tslogs.Minimum, float64(DefaultRefreshRate))\n\t\t\tk.refreshRateWarned = true\n\t\t}\n\t\treturn DefaultRefreshRate\n\t}\n\treturn rate\n}\n\n// RefreshDuration returns the refresh rate as a time.Duration.\nfunc (k *K9s) RefreshDuration() time.Duration {\n\treturn time.Duration(k.GetRefreshRate() * float32(time.Second))\n}\n\n// IsReadOnly returns the readonly setting.\nfunc (k *K9s) IsReadOnly() bool {\n\tro := k.ReadOnly\n\tif cfg := k.getActiveConfig(); cfg != nil && cfg.Context.ReadOnly != nil {\n\t\tro = *cfg.Context.ReadOnly\n\t}\n\tif k.manualReadOnly != nil {\n\t\tro = *k.manualReadOnly\n\t}\n\n\treturn ro\n}\n\n// Validate the current configuration.\nfunc (k *K9s) Validate(c client.Connection, contextName, clusterName string) {\n\tif k.RefreshRate <= 0 {\n\t\tk.RefreshRate = defaultRefreshRate\n\t}\n\tif k.MaxConnRetry <= 0 {\n\t\tk.MaxConnRetry = defaultMaxConnRetry\n\t}\n\n\tif a := os.Getenv(envPFAddress); a != \"\" {\n\t\tk.PortForwardAddress = a\n\t}\n\tif k.PortForwardAddress == \"\" {\n\t\tk.PortForwardAddress = defaultPFAddress()\n\t}\n\n\tif k.getActiveConfig() == nil {\n\t\t_, _ = k.ActivateContext(contextName)\n\t}\n\tif k.ShellPod != nil {\n\t\tk.ShellPod.Validate()\n\t}\n\tk.Logger = k.Logger.Validate()\n\tk.Thresholds = k.Thresholds.Validate()\n\n\tif cfg := k.getActiveConfig(); cfg != nil {\n\t\tcfg.Validate(c, contextName, clusterName)\n\t}\n}\n"
  },
  {
    "path": "internal/config/k9s_int_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_k9sOverrides(t *testing.T) {\n\tvar (\n\t\ttrueVal = true\n\t\tcmd     = \"po\"\n\t\tdir     = \"/tmp/blee\"\n\t)\n\n\tuu := map[string]struct {\n\t\tk                  *K9s\n\t\trate               float32\n\t\tro, hl, cl, sl, ll bool\n\t}{\n\t\t\"plain\": {\n\t\t\tk: &K9s{\n\t\t\t\tLiveViewAutoRefresh: false,\n\t\t\t\tScreenDumpDir:       \"\",\n\t\t\t\tRefreshRate:         10.0,\n\t\t\t\tMaxConnRetry:        0,\n\t\t\t\tReadOnly:            false,\n\t\t\t\tNoExitOnCtrlC:       false,\n\t\t\t\tUI:                  UI{},\n\t\t\t\tSkipLatestRevCheck:  false,\n\t\t\t\tDisablePodCounting:  false,\n\t\t\t},\n\t\t\trate: 10.0,\n\t\t},\n\t\t\"sub-second\": {\n\t\t\tk: &K9s{\n\t\t\t\tLiveViewAutoRefresh: false,\n\t\t\t\tScreenDumpDir:       \"\",\n\t\t\t\tRefreshRate:         0.5,\n\t\t\t\tMaxConnRetry:        0,\n\t\t\t\tReadOnly:            false,\n\t\t\t\tNoExitOnCtrlC:       false,\n\t\t\t\tUI:                  UI{},\n\t\t\t\tSkipLatestRevCheck:  false,\n\t\t\t\tDisablePodCounting:  false,\n\t\t\t},\n\t\t\trate: 2.0, // minimum enforced\n\t\t},\n\t\t\"set\": {\n\t\t\tk: &K9s{\n\t\t\t\tLiveViewAutoRefresh: false,\n\t\t\t\tScreenDumpDir:       \"\",\n\t\t\t\tRefreshRate:         10.0,\n\t\t\t\tMaxConnRetry:        0,\n\t\t\t\tReadOnly:            true,\n\t\t\t\tNoExitOnCtrlC:       false,\n\t\t\t\tUI: UI{\n\t\t\t\t\tHeadless:   true,\n\t\t\t\t\tLogoless:   true,\n\t\t\t\t\tCrumbsless: true,\n\t\t\t\t\tSplashless: true,\n\t\t\t\t},\n\t\t\t\tSkipLatestRevCheck: false,\n\t\t\t\tDisablePodCounting: false,\n\t\t\t},\n\t\t\trate: 10.0,\n\t\t\tro:   true,\n\t\t\thl:   true,\n\t\t\tll:   true,\n\t\t\tcl:   true,\n\t\t\tsl:   true,\n\t\t},\n\t\t\"overrides\": {\n\t\t\tk: &K9s{\n\t\t\t\tLiveViewAutoRefresh: false,\n\t\t\t\tScreenDumpDir:       \"\",\n\t\t\t\tRefreshRate:         10.0,\n\t\t\t\tMaxConnRetry:        0,\n\t\t\t\tReadOnly:            false,\n\t\t\t\tNoExitOnCtrlC:       false,\n\t\t\t\tUI: UI{\n\t\t\t\t\tHeadless:         false,\n\t\t\t\t\tLogoless:         false,\n\t\t\t\t\tCrumbsless:       false,\n\t\t\t\t\tmanualHeadless:   &trueVal,\n\t\t\t\t\tmanualLogoless:   &trueVal,\n\t\t\t\t\tmanualCrumbsless: &trueVal,\n\t\t\t\t\tmanualSplashless: &trueVal,\n\t\t\t\t},\n\t\t\t\tSkipLatestRevCheck:  false,\n\t\t\t\tDisablePodCounting:  false,\n\t\t\t\tmanualRefreshRate:   100.0,\n\t\t\t\tmanualReadOnly:      &trueVal,\n\t\t\t\tmanualCommand:       &cmd,\n\t\t\t\tmanualScreenDumpDir: &dir,\n\t\t\t},\n\t\t\trate: 100.0,\n\t\t\tro:   true,\n\t\t\thl:   true,\n\t\t\tll:   true,\n\t\t\tcl:   true,\n\t\t\tsl:   true,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.InDelta(t, u.rate, u.k.GetRefreshRate(), 0.001)\n\t\t\tassert.Equal(t, u.ro, u.k.IsReadOnly())\n\t\t\tassert.Equal(t, u.cl, u.k.IsCrumbsless())\n\t\t\tassert.Equal(t, u.sl, u.k.IsSplashless())\n\t\t\tassert.Equal(t, u.hl, u.k.IsHeadless())\n\t\t\tassert.Equal(t, u.ll, u.k.IsLogoless())\n\t\t})\n\t}\n}\n\nfunc Test_screenDumpDirOverride(t *testing.T) {\n\tuu := map[string]struct {\n\t\tdir string\n\t\te   string\n\t}{\n\t\t\"empty\": {\n\t\t\te: \"/tmp/k9s-test/screen-dumps\",\n\t\t},\n\t\t\"override\": {\n\t\t\tdir: \"/tmp/k9s-test/sd\",\n\t\t\te:   \"/tmp/k9s-test/sd\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tcfg := NewConfig(nil)\n\t\t\trequire.NoError(t, cfg.Load(\"testdata/configs/k9s.yaml\", true))\n\n\t\t\tcfg.K9s.manualScreenDumpDir = &u.dir\n\t\t\tassert.Equal(t, u.e, cfg.K9s.AppScreenDumpDir())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/config/k9s_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/config/mock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/cli-runtime/pkg/genericclioptions\"\n)\n\nfunc TestK9sReload(t *testing.T) {\n\tconfig.AppConfigDir = \"/tmp/k9s-test\"\n\n\tcl, ct := \"cl-1\", \"ct-1-1\"\n\n\tuu := map[string]struct {\n\t\tk      *config.K9s\n\t\tcl, ct string\n\t\terr    error\n\t}{\n\t\t\"no-context\": {\n\t\t\tk: config.NewK9s(\n\t\t\t\tmock.NewMockConnection(),\n\t\t\t\tmock.NewMockKubeSettings(&genericclioptions.ConfigFlags{\n\t\t\t\t\tClusterName: &cl,\n\t\t\t\t\tContext:     &ct,\n\t\t\t\t}),\n\t\t\t),\n\t\t\terr: errors.New(`no context found for: \"\"`),\n\t\t},\n\t\t\"set-context\": {\n\t\t\tk: config.NewK9s(\n\t\t\t\tmock.NewMockConnection(),\n\t\t\t\tmock.NewMockKubeSettings(&genericclioptions.ConfigFlags{\n\t\t\t\t\tClusterName: &cl,\n\t\t\t\t\tContext:     &ct,\n\t\t\t\t}),\n\t\t\t),\n\t\t\tct: \"ct-1-1\",\n\t\t\tcl: \"cl-1\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\t_, _ = u.k.ActivateContext(u.ct)\n\t\t\tassert.Equal(t, u.err, u.k.Reload())\n\t\t\tct, err := u.k.ActiveContext()\n\t\t\tassert.Equal(t, u.err, err)\n\t\t\tif err == nil {\n\t\t\t\tassert.Equal(t, u.cl, ct.ClusterName)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestK9sMerge(t *testing.T) {\n\tcl, ct := \"cl-1\", \"ct-1-1\"\n\n\tuu := map[string]struct {\n\t\tk1, k2 *config.K9s\n\t\tek     *config.K9s\n\t}{\n\t\t\"no-opt\": {\n\t\t\tk1: config.NewK9s(\n\t\t\t\tmock.NewMockConnection(),\n\t\t\t\tmock.NewMockKubeSettings(&genericclioptions.ConfigFlags{\n\t\t\t\t\tClusterName: &cl,\n\t\t\t\t\tContext:     &ct,\n\t\t\t\t}),\n\t\t\t),\n\t\t\tek: config.NewK9s(\n\t\t\t\tmock.NewMockConnection(),\n\t\t\t\tmock.NewMockKubeSettings(&genericclioptions.ConfigFlags{\n\t\t\t\t\tClusterName: &cl,\n\t\t\t\t\tContext:     &ct,\n\t\t\t\t}),\n\t\t\t),\n\t\t},\n\t\t\"override\": {\n\t\t\tk1: &config.K9s{\n\t\t\t\tLiveViewAutoRefresh: false,\n\t\t\t\tScreenDumpDir:       \"\",\n\t\t\t\tRefreshRate:         0,\n\t\t\t\tMaxConnRetry:        0,\n\t\t\t\tReadOnly:            false,\n\t\t\t\tNoExitOnCtrlC:       false,\n\t\t\t\tUI:                  config.UI{},\n\t\t\t\tSkipLatestRevCheck:  false,\n\t\t\t\tDisablePodCounting:  false,\n\t\t\t\tShellPod:            new(config.ShellPod),\n\t\t\t\tImageScans:          config.ImageScans{},\n\t\t\t\tLogger:              config.Logger{},\n\t\t\t\tThresholds:          nil,\n\t\t\t},\n\t\t\tk2: &config.K9s{\n\t\t\t\tLiveViewAutoRefresh: true,\n\t\t\t\tMaxConnRetry:        100,\n\t\t\t\tShellPod:            config.NewShellPod(),\n\t\t\t},\n\t\t\tek: &config.K9s{\n\t\t\t\tLiveViewAutoRefresh: true,\n\t\t\t\tScreenDumpDir:       \"\",\n\t\t\t\tRefreshRate:         0,\n\t\t\t\tMaxConnRetry:        100,\n\t\t\t\tReadOnly:            false,\n\t\t\t\tNoExitOnCtrlC:       false,\n\t\t\t\tUI:                  config.UI{},\n\t\t\t\tSkipLatestRevCheck:  false,\n\t\t\t\tDisablePodCounting:  false,\n\t\t\t\tShellPod:            config.NewShellPod(),\n\t\t\t\tImageScans:          config.ImageScans{},\n\t\t\t\tLogger:              config.Logger{},\n\t\t\t\tThresholds:          nil,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tu.k1.Merge(u.k2)\n\t\t\tassert.Equal(t, u.ek, u.k1)\n\t\t})\n\t}\n}\n\nfunc TestContextScreenDumpDir(t *testing.T) {\n\tcfg := mock.NewMockConfig(t)\n\t_, err := cfg.K9s.ActivateContext(\"ct-1-1\")\n\n\trequire.NoError(t, err)\n\trequire.NoError(t, cfg.Load(\"testdata/configs/k9s.yaml\", true))\n\tassert.Equal(t, \"/tmp/k9s-test/screen-dumps/cl-1/ct-1-1\", cfg.K9s.ContextScreenDumpDir())\n}\n\nfunc TestAppScreenDumpDir(t *testing.T) {\n\tcfg := mock.NewMockConfig(t)\n\n\trequire.NoError(t, cfg.Load(\"testdata/configs/k9s.yaml\", true))\n\tassert.Equal(t, \"/tmp/k9s-test/screen-dumps\", cfg.K9s.AppScreenDumpDir())\n}\n"
  },
  {
    "path": "internal/config/logger.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nconst (\n\t// DefaultLoggerTailCount tracks default log tail size.\n\tDefaultLoggerTailCount = 100\n\n\t// MaxLogThreshold sets the max value for log size.\n\tMaxLogThreshold = 5_000\n\n\t// DefaultSinceSeconds tracks default log age.\n\tDefaultSinceSeconds = -1 // tail logs by default\n)\n\n// Logger tracks logger options.\ntype Logger struct {\n\tTailCount         int64 `json:\"tail\" yaml:\"tail\"`\n\tBufferSize        int   `json:\"buffer\" yaml:\"buffer\"`\n\tSinceSeconds      int64 `json:\"sinceSeconds\" yaml:\"sinceSeconds\"`\n\tTextWrap          bool  `json:\"textWrap\" yaml:\"textWrap\"`\n\tDisableAutoscroll bool  `json:\"disableAutoscroll\" yaml:\"disableAutoscroll\"`\n\tColumnLock        bool  `json:\"columnLock\" yaml:\"columnLock\"`\n\tShowTime          bool  `json:\"showTime\" yaml:\"showTime\"`\n}\n\n// NewLogger returns a new instance.\nfunc NewLogger() Logger {\n\treturn Logger{\n\t\tTailCount:    DefaultLoggerTailCount,\n\t\tBufferSize:   MaxLogThreshold,\n\t\tSinceSeconds: DefaultSinceSeconds,\n\t}\n}\n\n// Validate checks thresholds and make sure we're cool. If not use defaults.\nfunc (l Logger) Validate() Logger {\n\tif l.TailCount <= 0 {\n\t\tl.TailCount = DefaultLoggerTailCount\n\t}\n\tif l.TailCount > MaxLogThreshold {\n\t\tl.TailCount = MaxLogThreshold\n\t}\n\tif l.BufferSize <= 0 || l.BufferSize > MaxLogThreshold {\n\t\tl.BufferSize = MaxLogThreshold\n\t}\n\tif l.SinceSeconds == 0 {\n\t\tl.SinceSeconds = DefaultSinceSeconds\n\t}\n\n\treturn l\n}\n"
  },
  {
    "path": "internal/config/logger_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNewLogger(t *testing.T) {\n\tl := config.NewLogger()\n\tl = l.Validate()\n\n\tassert.Equal(t, int64(100), l.TailCount)\n\tassert.Equal(t, 5000, l.BufferSize)\n}\n\nfunc TestLoggerValidate(t *testing.T) {\n\tvar l config.Logger\n\tl = l.Validate()\n\n\tassert.Equal(t, int64(100), l.TailCount)\n\tassert.Equal(t, 5000, l.BufferSize)\n}\n"
  },
  {
    "path": "internal/config/mock/test_helpers.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage mock\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/stretchr/testify/require\"\n\tversion \"k8s.io/apimachinery/pkg/version\"\n\t\"k8s.io/cli-runtime/pkg/genericclioptions\"\n\tdisk \"k8s.io/client-go/discovery/cached/disk\"\n\tdynamic \"k8s.io/client-go/dynamic\"\n\tkubernetes \"k8s.io/client-go/kubernetes\"\n\trestclient \"k8s.io/client-go/rest\"\n\t\"k8s.io/client-go/tools/clientcmd/api\"\n\tversioned \"k8s.io/metrics/pkg/client/clientset/versioned\"\n)\n\nfunc EnsureDir(d string) error {\n\tif _, err := os.Stat(d); errors.Is(err, fs.ErrNotExist) {\n\t\treturn os.MkdirAll(d, 0700)\n\t}\n\tif err := os.RemoveAll(d); err != nil {\n\t\treturn err\n\t}\n\n\treturn os.MkdirAll(d, 0700)\n}\n\nfunc NewMockConfig(t testing.TB) *config.Config {\n\tif _, err := os.Stat(\"/tmp/test\"); err == nil {\n\t\tif e := os.RemoveAll(\"/tmp/test\"); e != nil {\n\t\t\trequire.NoError(t, e)\n\t\t}\n\t}\n\tconfig.AppContextsDir = \"/tmp/test\"\n\tcl, ct := \"cl-1\", \"ct-1-1\"\n\tflags := genericclioptions.ConfigFlags{\n\t\tClusterName: &cl,\n\t\tContext:     &ct,\n\t}\n\tcfg := config.NewConfig(\n\t\tNewMockKubeSettings(&flags),\n\t)\n\n\treturn cfg\n}\n\ntype mockKubeSettings struct {\n\tflags *genericclioptions.ConfigFlags\n\tcts   map[string]*api.Context\n}\n\nfunc NewMockKubeSettings(f *genericclioptions.ConfigFlags) mockKubeSettings {\n\t_, idx, _ := strings.Cut(*f.ClusterName, \"-\")\n\tctId := \"ct-\" + idx\n\n\treturn mockKubeSettings{\n\t\tflags: f,\n\t\tcts: map[string]*api.Context{\n\t\t\tctId + \"-1\": {\n\t\t\t\tCluster:   *f.ClusterName,\n\t\t\t\tNamespace: \"\",\n\t\t\t},\n\t\t\tctId + \"-2\": {\n\t\t\t\tCluster:   *f.ClusterName,\n\t\t\t\tNamespace: \"ns-2\",\n\t\t\t},\n\t\t\tctId + \"-3\": {\n\t\t\t\tCluster:   *f.ClusterName,\n\t\t\t\tNamespace: client.DefaultNamespace,\n\t\t\t},\n\t\t\t\"fred-blee\": {\n\t\t\t\tCluster:   \"arn:aws:eks:eu-central-1:xxx:cluster/fred-blee\",\n\t\t\t\tNamespace: client.DefaultNamespace,\n\t\t\t},\n\t\t},\n\t}\n}\nfunc (m mockKubeSettings) CurrentContextName() (string, error) {\n\treturn *m.flags.Context, nil\n}\nfunc (m mockKubeSettings) CurrentClusterName() (string, error) {\n\treturn *m.flags.ClusterName, nil\n}\nfunc (mockKubeSettings) CurrentNamespaceName() (string, error) {\n\treturn \"default\", nil\n}\nfunc (m mockKubeSettings) GetContext(s string) (*api.Context, error) {\n\tct, ok := m.cts[s]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"no context found for: %q\", s)\n\t}\n\treturn ct, nil\n}\nfunc (m mockKubeSettings) CurrentContext() (*api.Context, error) {\n\treturn m.GetContext(*m.flags.Context)\n}\nfunc (m mockKubeSettings) ContextNames() (map[string]struct{}, error) {\n\tmm := make(map[string]struct{}, len(m.cts))\n\tfor k := range m.cts {\n\t\tmm[k] = struct{}{}\n\t}\n\n\treturn mm, nil\n}\n\nfunc (mockKubeSettings) SetProxy(func(*http.Request) (*url.URL, error)) {}\n\ntype mockConnection struct {\n\tct string\n}\n\nfunc NewMockConnection() mockConnection {\n\treturn mockConnection{}\n}\nfunc NewMockConnectionWithContext(ct string) mockConnection {\n\treturn mockConnection{ct: ct}\n}\n\nfunc (mockConnection) CanI(string, *client.GVR, string, []string) (bool, error) {\n\treturn true, nil\n}\nfunc (mockConnection) Config() *client.Config {\n\treturn nil\n}\nfunc (mockConnection) ConnectionOK() bool {\n\treturn false\n}\nfunc (mockConnection) Dial() (kubernetes.Interface, error) {\n\treturn nil, nil\n}\nfunc (mockConnection) DialLogs() (kubernetes.Interface, error) {\n\treturn nil, nil\n}\nfunc (mockConnection) SwitchContext(string) error {\n\treturn nil\n}\nfunc (mockConnection) CachedDiscovery() (*disk.CachedDiscoveryClient, error) {\n\treturn nil, nil\n}\nfunc (mockConnection) RestConfig() (*restclient.Config, error) {\n\treturn nil, nil\n}\nfunc (mockConnection) MXDial() (*versioned.Clientset, error) {\n\treturn nil, nil\n}\nfunc (mockConnection) DynDial() (dynamic.Interface, error) {\n\treturn nil, nil\n}\nfunc (mockConnection) HasMetrics() bool {\n\treturn false\n}\nfunc (mockConnection) ValidNamespaceNames() (client.NamespaceNames, error) {\n\treturn nil, nil\n}\nfunc (mockConnection) IsValidNamespace(string) bool {\n\treturn true\n}\nfunc (mockConnection) ServerVersion() (*version.Info, error) {\n\treturn nil, nil\n}\nfunc (mockConnection) CheckConnectivity() bool {\n\treturn false\n}\nfunc (m mockConnection) ActiveContext() string {\n\treturn m.ct\n}\nfunc (mockConnection) ActiveNamespace() string {\n\treturn \"\"\n}\nfunc (mockConnection) IsActiveNamespace(string) bool {\n\treturn false\n}\n"
  },
  {
    "path": "internal/config/plugin.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/adrg/xdg\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/config/json\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/karrick/godirwalk\"\n\t\"gopkg.in/yaml.v3\"\n)\n\ntype plugins map[string]Plugin\n\n// Plugins represents a collection of plugins.\ntype Plugins struct {\n\tPlugins plugins `yaml:\"plugins\"`\n}\n\n// PluginInputType represents the type of input field.\ntype PluginInputType string\n\nconst (\n\tInputTypeString   PluginInputType = \"string\"\n\tInputTypeNumber   PluginInputType = \"number\"\n\tInputTypeBool     PluginInputType = \"bool\"\n\tInputTypeDropdown PluginInputType = \"dropdown\"\n)\n\n// PluginInput describes an input field for a plugin.\ntype PluginInput struct {\n\tName     string          `yaml:\"name\"`\n\tLabel    string          `yaml:\"label\"`\n\tType     PluginInputType `yaml:\"type\"`\n\tRequired bool            `yaml:\"required\"`\n\tDefault  string          `yaml:\"default\"`\n\tOptions  []string        `yaml:\"options\"`\n}\n\n// Plugin describes a K9s plugin.\ntype Plugin struct {\n\tScopes          []string      `yaml:\"scopes\"`\n\tArgs            []string      `yaml:\"args\"`\n\tShortCut        string        `yaml:\"shortCut\"`\n\tOverride        bool          `yaml:\"override\"`\n\tPipes           []string      `yaml:\"pipes\"`\n\tDescription     string        `yaml:\"description\"`\n\tCommand         string        `yaml:\"command\"`\n\tConfirm         *bool         `yaml:\"confirm\"`\n\tBackground      bool          `yaml:\"background\"`\n\tDangerous       bool          `yaml:\"dangerous\"`\n\tOverwriteOutput bool          `yaml:\"overwriteOutput\"`\n\tInputs          []PluginInput `yaml:\"inputs\"`\n}\n\nfunc (p Plugin) String() string {\n\treturn fmt.Sprintf(\"[%s] %s(%s)\", p.ShortCut, p.Command, strings.Join(p.Args, \" \"))\n}\n\n// ShouldConfirm returns whether the plugin should show a confirmation dialog.\n// Defaults to true when inputs are defined, false otherwise.\nfunc (p *Plugin) ShouldConfirm() bool {\n\tif p.Confirm != nil {\n\t\treturn *p.Confirm\n\t}\n\treturn len(p.Inputs) > 0\n}\n\n// Validate checks the plugin configuration for errors.\nfunc (p *Plugin) Validate() error {\n\tseen := make(map[string]struct{}, len(p.Inputs))\n\tfor _, input := range p.Inputs {\n\t\tif _, ok := seen[input.Name]; ok {\n\t\t\treturn fmt.Errorf(\"duplicate input name %q\", input.Name)\n\t\t}\n\t\tseen[input.Name] = struct{}{}\n\n\t\tif input.Default == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch input.Type {\n\t\tcase InputTypeDropdown:\n\t\t\tif !slices.Contains(input.Options, input.Default) {\n\t\t\t\treturn fmt.Errorf(\"default value %q for input %q is not a valid option\", input.Default, input.Name)\n\t\t\t}\n\t\tcase InputTypeBool:\n\t\t\tif input.Default != \"true\" && input.Default != \"false\" {\n\t\t\t\treturn fmt.Errorf(\"default value %q for bool input %q must be \\\"true\\\" or \\\"false\\\"\", input.Default, input.Name)\n\t\t\t}\n\t\tcase InputTypeNumber:\n\t\t\tif _, err := strconv.ParseFloat(input.Default, 64); err != nil {\n\t\t\t\treturn fmt.Errorf(\"default value %q for number input %q is not a valid number\", input.Default, input.Name)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// NewPlugins returns a new plugin.\nfunc NewPlugins() Plugins {\n\treturn Plugins{\n\t\tPlugins: make(map[string]Plugin),\n\t}\n}\n\n// Load K9s plugins.\nfunc (p Plugins) Load(path string, loadExtra bool) error {\n\tvar errs error\n\n\t// Load from global config file\n\tif err := p.load(AppPluginsFile); err != nil {\n\t\terrs = errors.Join(errs, err)\n\t}\n\n\t// Load from cluster/context config\n\tif err := p.load(path); err != nil {\n\t\terrs = errors.Join(errs, err)\n\t}\n\n\tif !loadExtra {\n\t\treturn errs\n\t}\n\t// Load from XDG dirs\n\tconst k9sPluginsDir = \"k9s/plugins\"\n\tfor _, dir := range append(xdg.DataDirs, xdg.DataHome, xdg.ConfigHome) {\n\t\tpath := filepath.Join(dir, k9sPluginsDir)\n\t\tif err := p.loadDir(path); err != nil {\n\t\t\terrs = errors.Join(errs, err)\n\t\t}\n\t}\n\n\treturn errs\n}\n\nfunc (p *Plugins) load(path string) error {\n\tif _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {\n\t\treturn nil\n\t}\n\tbb, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tscheme, err := data.JSONValidator.ValidatePlugins(bb)\n\tif err != nil {\n\t\tslog.Warn(\"Plugin schema validation failed\",\n\t\t\tslogs.Path, path,\n\t\t\tslogs.Error, err,\n\t\t)\n\t\treturn fmt.Errorf(\"plugin validation failed for %s: %w\", path, err)\n\t}\n\n\td := yaml.NewDecoder(bytes.NewReader(bb))\n\td.KnownFields(true)\n\n\tswitch scheme {\n\tcase json.PluginSchema:\n\t\tvar o Plugin\n\t\tif err := yaml.Unmarshal(bb, &o); err != nil {\n\t\t\treturn fmt.Errorf(\"plugin unmarshal failed for %s: %w\", path, err)\n\t\t}\n\t\tif err := o.Validate(); err != nil {\n\t\t\treturn fmt.Errorf(\"plugin validation failed for %s: %w\", path, err)\n\t\t}\n\t\tp.Plugins[strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))] = o\n\tcase json.PluginsSchema:\n\t\tvar oo Plugins\n\t\tif err := yaml.Unmarshal(bb, &oo); err != nil {\n\t\t\treturn fmt.Errorf(\"plugin unmarshal failed for %s: %w\", path, err)\n\t\t}\n\t\tfor k := range oo.Plugins {\n\t\t\tplug := oo.Plugins[k]\n\t\t\tif err := plug.Validate(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"plugin %q validation failed for %s: %w\", k, path, err)\n\t\t\t}\n\t\t\tp.Plugins[k] = plug\n\t\t}\n\tcase json.PluginMultiSchema:\n\t\tvar oo plugins\n\t\tif err := yaml.Unmarshal(bb, &oo); err != nil {\n\t\t\treturn fmt.Errorf(\"plugin unmarshal failed for %s: %w\", path, err)\n\t\t}\n\t\tfor k := range oo {\n\t\t\tplug := oo[k]\n\t\t\tif err := plug.Validate(); err != nil {\n\t\t\t\treturn fmt.Errorf(\"plugin %q validation failed for %s: %w\", k, path, err)\n\t\t\t}\n\t\t\tp.Plugins[k] = plug\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (p Plugins) loadDir(dir string) error {\n\tif _, err := os.Stat(dir); errors.Is(err, fs.ErrNotExist) {\n\t\treturn nil\n\t}\n\n\tvar errs error\n\terrs = errors.Join(errs, godirwalk.Walk(dir, &godirwalk.Options{\n\t\tFollowSymbolicLinks: true,\n\t\tCallback: func(path string, de *godirwalk.Dirent) error {\n\t\t\tif de.IsDir() || !isYamlFile(de.Name()) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\terrs = errors.Join(errs, p.load(path))\n\t\t\treturn nil\n\t\t},\n\t\tErrorCallback: func(osPathname string, err error) godirwalk.ErrorAction {\n\t\t\tslog.Warn(\"Error at %s: %v - skipping node\", slogs.Path, osPathname, slogs.Error, err)\n\t\t\treturn godirwalk.SkipNode\n\t\t},\n\t}))\n\n\treturn errs\n}\n"
  },
  {
    "path": "internal/config/plugin_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPluginLoad(t *testing.T) {\n\tuu := map[string]struct {\n\t\tpath string\n\t\terr  string\n\t\tee   Plugins\n\t}{\n\t\t\"snippet\": {\n\t\t\tpath: \"testdata/plugins/dir/snippet.1.yaml\",\n\t\t\tee: Plugins{\n\t\t\t\tPlugins: plugins{\n\t\t\t\t\t\"snippet.1\": Plugin{\n\t\t\t\t\t\tScopes:          []string{\"po\", \"dp\"},\n\t\t\t\t\t\tArgs:            []string{\"-n\", \"$NAMESPACE\", \"-boolean\"},\n\t\t\t\t\t\tShortCut:        \"shift-s\",\n\t\t\t\t\t\tDescription:     \"blee\",\n\t\t\t\t\t\tCommand:         \"duh\",\n\t\t\t\t\t\tConfirm:         boolPtr(true),\n\t\t\t\t\t\tOverwriteOutput: true,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"multi-snippets\": {\n\t\t\tpath: \"testdata/plugins/dir/snippet.multi.yaml\",\n\t\t\tee: Plugins{\n\t\t\t\tPlugins: plugins{\n\t\t\t\t\t\"crapola\": Plugin{\n\t\t\t\t\t\tShortCut:    \"Shift-1\",\n\t\t\t\t\t\tCommand:     \"crapola\",\n\t\t\t\t\t\tDescription: \"crapola\",\n\t\t\t\t\t\tScopes:      []string{\"pods\"},\n\t\t\t\t\t},\n\t\t\t\t\t\"bozo\": Plugin{\n\t\t\t\t\t\tShortCut:    \"Shift-2\",\n\t\t\t\t\t\tDescription: \"bozo\",\n\t\t\t\t\t\tCommand:     \"bozo\",\n\t\t\t\t\t\tScopes:      []string{\"pods\", \"svc\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"full\": {\n\t\t\tpath: \"testdata/plugins/plugins.yaml\",\n\t\t\tee: Plugins{\n\t\t\t\tPlugins: plugins{\n\t\t\t\t\t\"blah\": Plugin{\n\t\t\t\t\t\tScopes:      []string{\"po\", \"dp\"},\n\t\t\t\t\t\tArgs:        []string{\"-n\", \"$NAMESPACE\", \"-boolean\"},\n\t\t\t\t\t\tShortCut:    \"shift-s\",\n\t\t\t\t\t\tDescription: \"blee\",\n\t\t\t\t\t\tCommand:     \"duh\",\n\t\t\t\t\t\tConfirm:     boolPtr(true),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"toast-no-file\": {\n\t\t\tpath: \"testdata/plugins/plugins-bozo.yaml\",\n\t\t\tee:   NewPlugins(),\n\t\t},\n\n\t\t\"toast-invalid\": {\n\t\t\tpath: \"testdata/plugins/plugins-toast.yaml\",\n\t\t\tee:   NewPlugins(),\n\t\t\terr:  \"plugin validation failed for testdata/plugins/plugins-toast.yaml: scopes is required\\nAdditional property plugins is not allowed\\ncommand is required\\ndescription is required\\nscopes is required\\nshortCut is required\\ncommand is required\\ndescription is required\\nscopes is required\\nshortCut is required\",\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tp := NewPlugins()\n\t\t\terr := p.Load(u.path, false)\n\t\t\tif err != nil {\n\t\t\t\tassert.Equal(t, u.err, err.Error())\n\t\t\t}\n\t\t\tassert.Equal(t, u.ee, p)\n\t\t})\n\t}\n}\n\nfunc TestSinglePluginFileLoad(t *testing.T) {\n\te := Plugin{\n\t\tScopes:      []string{\"po\", \"dp\"},\n\t\tArgs:        []string{\"-n\", \"$NAMESPACE\", \"-boolean\"},\n\t\tShortCut:    \"shift-s\",\n\t\tDescription: \"blee\",\n\t\tCommand:     \"duh\",\n\t\tConfirm:     boolPtr(true),\n\t}\n\n\tp := NewPlugins()\n\trequire.NoError(t, p.load(\"testdata/plugins/plugins.yaml\"))\n\trequire.NoError(t, p.loadDir(\"/random/dir/not/exist\"))\n\n\tassert.Len(t, p.Plugins, 1)\n\tv, ok := p.Plugins[\"blah\"]\n\n\tassert.True(t, ok)\n\tassert.ObjectsAreEqual(e, v)\n}\n\nfunc TestMultiplePluginFilesLoad(t *testing.T) {\n\tuu := map[string]struct {\n\t\tpath string\n\t\tdir  string\n\t\tee   Plugins\n\t}{\n\t\t\"empty\": {\n\t\t\tpath: \"testdata/plugins/plugins.yaml\",\n\t\t\tdir:  \"testdata/plugins/dir\",\n\t\t\tee: Plugins{\n\t\t\t\tPlugins: plugins{\n\t\t\t\t\t\"blah\": {\n\t\t\t\t\t\tScopes:      []string{\"po\", \"dp\"},\n\t\t\t\t\t\tArgs:        []string{\"-n\", \"$NAMESPACE\", \"-boolean\"},\n\t\t\t\t\t\tShortCut:    \"shift-s\",\n\t\t\t\t\t\tDescription: \"blee\",\n\t\t\t\t\t\tCommand:     \"duh\",\n\t\t\t\t\t\tConfirm:     boolPtr(true),\n\t\t\t\t\t},\n\t\t\t\t\t\"snippet.1\": {\n\t\t\t\t\t\tShortCut:        \"shift-s\",\n\t\t\t\t\t\tCommand:         \"duh\",\n\t\t\t\t\t\tScopes:          []string{\"po\", \"dp\"},\n\t\t\t\t\t\tArgs:            []string{\"-n\", \"$NAMESPACE\", \"-boolean\"},\n\t\t\t\t\t\tDescription:     \"blee\",\n\t\t\t\t\t\tConfirm:         boolPtr(true),\n\t\t\t\t\t\tOverwriteOutput: true,\n\t\t\t\t\t},\n\t\t\t\t\t\"snippet.2\": {\n\t\t\t\t\t\tScopes:      []string{\"svc\", \"ing\"},\n\t\t\t\t\t\tArgs:        []string{\"-n\", \"$NAMESPACE\", \"-oyaml\"},\n\t\t\t\t\t\tShortCut:    \"shift-r\",\n\t\t\t\t\t\tDescription: \"bla\",\n\t\t\t\t\t\tCommand:     \"duha\",\n\t\t\t\t\t\tConfirm:     boolPtr(false),\n\t\t\t\t\t\tBackground:  true,\n\t\t\t\t\t},\n\t\t\t\t\t\"crapola\": {\n\t\t\t\t\t\tScopes:      []string{\"pods\"},\n\t\t\t\t\t\tCommand:     \"crapola\",\n\t\t\t\t\t\tDescription: \"crapola\",\n\t\t\t\t\t\tShortCut:    \"Shift-1\",\n\t\t\t\t\t},\n\t\t\t\t\t\"bozo\": {\n\t\t\t\t\t\tScopes:      []string{\"pods\", \"svc\"},\n\t\t\t\t\t\tCommand:     \"bozo\",\n\t\t\t\t\t\tDescription: \"bozo\",\n\t\t\t\t\t\tShortCut:    \"Shift-2\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tp := NewPlugins()\n\t\t\trequire.NoError(t, p.load(u.path))\n\t\t\trequire.NoError(t, p.loadDir(u.dir))\n\t\t\tassert.Equal(t, u.ee, p)\n\t\t})\n\t}\n}\n\nfunc TestPluginLoadSymlink(t *testing.T) {\n\ttmp := t.TempDir()\n\n\tlinkFile := filepath.Join(tmp, \"plugins-symlink.yaml\")\n\twd, err := os.Getwd()\n\trequire.NoError(t, err)\n\trequire.NoError(t, os.Symlink(filepath.Join(wd, \"testdata\", \"plugins\", \"plugins.yaml\"), linkFile))\n\n\tlinkDir := filepath.Join(tmp, \"plugins-dir-symlink\")\n\trequire.NoError(t, os.Symlink(filepath.Join(wd, \"testdata\", \"plugins\", \"dir\"), linkDir))\n\n\t// Add a symlink with an infinite loop\n\tloopDir := filepath.Join(tmp, \"loop\")\n\trequire.NoError(t, os.Mkdir(loopDir, 0o755))\n\trequire.NoError(t, os.Symlink(loopDir, filepath.Join(loopDir, \"self\")))\n\n\tp := NewPlugins()\n\trequire.NoError(t, p.loadDir(tmp))\n\n\tee := Plugins{\n\t\tPlugins: plugins{\n\t\t\t\"blah\": Plugin{\n\t\t\t\tScopes:      []string{\"po\", \"dp\"},\n\t\t\t\tArgs:        []string{\"-n\", \"$NAMESPACE\", \"-boolean\"},\n\t\t\t\tShortCut:    \"shift-s\",\n\t\t\t\tDescription: \"blee\",\n\t\t\t\tCommand:     \"duh\",\n\t\t\t\tConfirm:     boolPtr(true),\n\t\t\t},\n\t\t\t\"snippet.1\": {\n\t\t\t\tShortCut:        \"shift-s\",\n\t\t\t\tCommand:         \"duh\",\n\t\t\t\tScopes:          []string{\"po\", \"dp\"},\n\t\t\t\tArgs:            []string{\"-n\", \"$NAMESPACE\", \"-boolean\"},\n\t\t\t\tDescription:     \"blee\",\n\t\t\t\tConfirm:         boolPtr(true),\n\t\t\t\tOverwriteOutput: true,\n\t\t\t},\n\t\t\t\"snippet.2\": {\n\t\t\t\tScopes:      []string{\"svc\", \"ing\"},\n\t\t\t\tArgs:        []string{\"-n\", \"$NAMESPACE\", \"-oyaml\"},\n\t\t\t\tShortCut:    \"shift-r\",\n\t\t\t\tDescription: \"bla\",\n\t\t\t\tCommand:     \"duha\",\n\t\t\t\tConfirm:     boolPtr(false),\n\t\t\t\tBackground:  true,\n\t\t\t},\n\t\t\t\"crapola\": {\n\t\t\t\tScopes:      []string{\"pods\"},\n\t\t\t\tCommand:     \"crapola\",\n\t\t\t\tDescription: \"crapola\",\n\t\t\t\tShortCut:    \"Shift-1\",\n\t\t\t},\n\t\t\t\"bozo\": {\n\t\t\t\tScopes:      []string{\"pods\", \"svc\"},\n\t\t\t\tCommand:     \"bozo\",\n\t\t\t\tDescription: \"bozo\",\n\t\t\t\tShortCut:    \"Shift-2\",\n\t\t\t},\n\t\t},\n\t}\n\n\tassert.Equal(t, ee, p)\n}\n"
  },
  {
    "path": "internal/config/refresh_rate_test.go",
    "content": "package config\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc TestRefreshRateBackwardCompatibility(t *testing.T) {\n\ttests := map[string]struct {\n\t\tyamlContent string\n\t\texpected    float32\n\t}{\n\t\t\"integer_value\": {\n\t\t\tyamlContent: `refreshRate: 2`,\n\t\t\texpected:    2.0,\n\t\t},\n\t\t\"float_value\": {\n\t\t\tyamlContent: `refreshRate: 2.5`,\n\t\t\texpected:    2.5,\n\t\t},\n\t}\n\n\tfor name, test := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tvar k K9s\n\t\t\terr := yaml.Unmarshal([]byte(test.yamlContent), &k)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.InDelta(t, test.expected, k.RefreshRate, 0.001)\n\t\t})\n\t}\n}\n\nfunc TestGetRefreshRateMinimum(t *testing.T) {\n\ttests := map[string]struct {\n\t\trefreshRate       float32\n\t\tmanualRefreshRate float32\n\t\texpected          float32\n\t}{\n\t\t\"below_minimum\": {\n\t\t\trefreshRate: 0.5,\n\t\t\texpected:    2.0,\n\t\t},\n\t\t\"at_minimum\": {\n\t\t\trefreshRate: 2.0,\n\t\t\texpected:    2.0,\n\t\t},\n\t\t\"above_minimum\": {\n\t\t\trefreshRate: 3.5,\n\t\t\texpected:    3.5,\n\t\t},\n\t\t\"manual_below_minimum\": {\n\t\t\trefreshRate:       3.0,\n\t\t\tmanualRefreshRate: 0.5,\n\t\t\texpected:          2.0,\n\t\t},\n\t\t\"manual_above_minimum\": {\n\t\t\trefreshRate:       2.0,\n\t\t\tmanualRefreshRate: 4.0,\n\t\t\texpected:          4.0,\n\t\t},\n\t}\n\n\tfor name, test := range tests {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\tk := K9s{\n\t\t\t\tRefreshRate:       test.refreshRate,\n\t\t\t\tmanualRefreshRate: test.manualRefreshRate,\n\t\t\t}\n\t\t\tassert.InDelta(t, test.expected, k.GetRefreshRate(), 0.001)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/config/scans.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\n// Labels tracks a collection of labels.\ntype Labels map[string][]string\n\nfunc (l Labels) exclude(k, val string) bool {\n\tvv, ok := l[k]\n\tif !ok {\n\t\treturn false\n\t}\n\n\tfor _, v := range vv {\n\t\tif v == val {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// ScanExcludes tracks vul scan exclusions.\ntype ScanExcludes struct {\n\tNamespaces []string `json:\"namespaces\" yaml:\"namespaces\"`\n\tLabels     Labels   `json:\"labels\" yaml:\"labels\"`\n}\n\nfunc newScanExcludes() ScanExcludes {\n\treturn ScanExcludes{\n\t\tLabels: make(Labels),\n\t}\n}\n\nfunc (b ScanExcludes) exclude(ns string, ll map[string]string) bool {\n\tfor _, nss := range b.Namespaces {\n\t\tif nss == ns {\n\t\t\treturn true\n\t\t}\n\t}\n\tfor k, v := range ll {\n\t\tif b.Labels.exclude(k, v) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// ImageScans tracks vul scans options.\ntype ImageScans struct {\n\tEnable     bool         `json:\"enable\" yaml:\"enable\"`\n\tExclusions ScanExcludes `json:\"exclusions\" yaml:\"exclusions\"`\n}\n\n// NewImageScans returns a new instance.\nfunc NewImageScans() ImageScans {\n\treturn ImageScans{\n\t\tExclusions: newScanExcludes(),\n\t}\n}\n\n// ShouldExclude checks if scan should be excluded given ns/labels\nfunc (i ImageScans) ShouldExclude(ns string, ll map[string]string) bool {\n\tif !i.Enable {\n\t\treturn false\n\t}\n\n\treturn i.Exclusions.exclude(ns, ll)\n}\n"
  },
  {
    "path": "internal/config/scans_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestScansShouldExclude(t *testing.T) {\n\tuu := map[string]struct {\n\t\tsc config.ImageScans\n\t\tns string\n\t\tll map[string]string\n\t\te  bool\n\t}{\n\t\t\"empty\": {\n\t\t\tsc: config.NewImageScans(),\n\t\t},\n\t\t\"exclude-ns\": {\n\t\t\tsc: config.ImageScans{\n\t\t\t\tEnable: true,\n\t\t\t\tExclusions: config.ScanExcludes{\n\t\t\t\t\tNamespaces: []string{\"ns-1\", \"ns-2\", \"ns-3\"},\n\t\t\t\t\tLabels: config.Labels{\n\t\t\t\t\t\t\"app\": []string{\"fred\", \"blee\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tns: \"ns-1\",\n\t\t\tll: map[string]string{\n\t\t\t\t\"app\": \"freddy\",\n\t\t\t},\n\t\t\te: true,\n\t\t},\n\t\t\"include-ns\": {\n\t\t\tsc: config.ImageScans{\n\t\t\t\tEnable: true,\n\t\t\t\tExclusions: config.ScanExcludes{\n\t\t\t\t\tNamespaces: []string{\"ns-1\", \"ns-2\", \"ns-3\"},\n\t\t\t\t\tLabels: config.Labels{\n\t\t\t\t\t\t\"app\": []string{\"fred\", \"blee\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tns: \"ns-4\",\n\t\t\tll: map[string]string{\n\t\t\t\t\"app\": \"bozo\",\n\t\t\t},\n\t\t},\n\t\t\"exclude-labels\": {\n\t\t\tsc: config.ImageScans{\n\t\t\t\tEnable: true,\n\t\t\t\tExclusions: config.ScanExcludes{\n\t\t\t\t\tNamespaces: []string{\"ns-1\", \"ns-2\", \"ns-3\"},\n\t\t\t\t\tLabels: config.Labels{\n\t\t\t\t\t\t\"app\": []string{\"fred\", \"blee\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tns: \"ns-4\",\n\t\t\tll: map[string]string{\n\t\t\t\t\"app\": \"fred\",\n\t\t\t},\n\t\t\te: true,\n\t\t},\n\t\t\"include-labels\": {\n\t\t\tsc: config.ImageScans{\n\t\t\t\tEnable: true,\n\t\t\t\tExclusions: config.ScanExcludes{\n\t\t\t\t\tNamespaces: []string{\"ns-1\", \"ns-2\", \"ns-3\"},\n\t\t\t\t\tLabels: config.Labels{\n\t\t\t\t\t\t\"app\": []string{\"fred\", \"blee\"},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tns: \"ns-4\",\n\t\t\tll: map[string]string{\n\t\t\t\t\"app\": \"freddy\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.sc.ShouldExclude(u.ns, u.ll))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/config/shell_pod.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nimport (\n\tv1 \"k8s.io/api/core/v1\"\n)\n\nconst defaultDockerShellImage = \"busybox:1.37.0\"\n\n// Limits represents resource limits.\ntype Limits map[v1.ResourceName]string\n\n// ShellPod represents k9s shell configuration.\ntype ShellPod struct {\n\tImage            string                    `json:\"image\" yaml:\"image\"`\n\tCommand          []string                  `json:\"command,omitempty\" yaml:\"command,omitempty\"`\n\tArgs             []string                  `json:\"args,omitempty\" yaml:\"args,omitempty\"`\n\tNamespace        string                    `json:\"namespace\" yaml:\"namespace\"`\n\tLimits           Limits                    `json:\"limits,omitempty\" yaml:\"limits,omitempty\"`\n\tLabels           map[string]string         `json:\"labels,omitempty\" yaml:\"labels,omitempty\"`\n\tImagePullSecrets []v1.LocalObjectReference `json:\"imagePullSecrets,omitempty\" yaml:\"imagePullSecrets,omitempty\"`\n\tImagePullPolicy  v1.PullPolicy             `json:\"imagePullPolicy,omitempty\" yaml:\"imagePullPolicy,omitempty\"`\n\tTTY              bool                      `json:\"tty,omitempty\" yaml:\"tty,omitempty\"`\n\tHostPathVolume   []hostPathVolume          `json:\"hostPathVolume,omitempty\" yaml:\"hostPathVolume,omitempty\"`\n}\n\ntype hostPathVolume struct {\n\tName      string `json:\"name\" yaml:\"name\"`\n\tMountPath string `json:\"mountPath\" yaml:\"mountPath\"`\n\tHostPath  string `json:\"hostPath\" yaml:\"hostPath\"`\n\tReadOnly  bool   `json:\"readOnly,omitempty\" yaml:\"readOnly,omitempty\"`\n}\n\n// NewShellPod returns a new instance.\nfunc NewShellPod() *ShellPod {\n\treturn &ShellPod{\n\t\tImage:     defaultDockerShellImage,\n\t\tNamespace: \"default\",\n\t\tLimits:    defaultLimits(),\n\t}\n}\n\n// Validate validates the configuration.\nfunc (s *ShellPod) Validate() {\n\tif s.Image == \"\" {\n\t\ts.Image = defaultDockerShellImage\n\t}\n\tif len(s.Limits) == 0 {\n\t\ts.Limits = defaultLimits()\n\t}\n}\n\nfunc defaultLimits() Limits {\n\treturn Limits{\n\t\tv1.ResourceCPU:    \"100m\",\n\t\tv1.ResourceMemory: \"100Mi\",\n\t}\n}\n"
  },
  {
    "path": "internal/config/styles.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/config/json\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\t\"gopkg.in/yaml.v3\"\n)\n\n// StyleListener represents a skin's listener.\ntype StyleListener interface {\n\t// StylesChanged notifies listener the skin changed.\n\tStylesChanged(*Styles)\n}\n\n// TextStyle tracks text styles.\ntype TextStyle string\n\nconst (\n\t// TextStyleNormal is the default text style.\n\tTextStyleNormal TextStyle = \"normal\"\n\n\t// TextStyleBold is the bold text style.\n\tTextStyleBold TextStyle = \"bold\"\n\n\t// TextStyleDim is the dim text style.\n\tTextStyleDim TextStyle = \"dim\"\n)\n\n// ToShortString returns a short string representation of the text style.\nfunc (ts TextStyle) ToShortString() string {\n\tswitch ts {\n\tcase TextStyleNormal:\n\t\treturn \"-\"\n\tcase TextStyleBold:\n\t\treturn \"b\"\n\tcase TextStyleDim:\n\t\treturn \"d\"\n\tdefault:\n\t\treturn \"d\"\n\t}\n}\n\ntype (\n\t// Styles tracks K9s styling options.\n\tStyles struct {\n\t\tK9s       Style `json:\"k9s\" yaml:\"k9s\"`\n\t\tlisteners []StyleListener\n\t}\n\n\t// Style tracks K9s styles.\n\tStyle struct {\n\t\tBody   Body   `json:\"body\" yaml:\"body\"`\n\t\tPrompt Prompt `json:\"prompt\" yaml:\"prompt\"`\n\t\tHelp   Help   `json:\"help\" yaml:\"help\"`\n\t\tFrame  Frame  `json:\"frame\" yaml:\"frame\"`\n\t\tInfo   Info   `json:\"info\" yaml:\"info\"`\n\t\tViews  Views  `json:\"views\" yaml:\"views\"`\n\t\tDialog Dialog `json:\"dialog\" yaml:\"dialog\"`\n\t}\n\n\t// Prompt tracks command styles\n\tPrompt struct {\n\t\tFgColor      Color        `json:\"fgColor\" yaml:\"fgColor\"`\n\t\tBgColor      Color        `json:\"bgColor\" yaml:\"bgColor\"`\n\t\tSuggestColor Color        `json:\"\" yaml:\"suggestColor\"`\n\t\tBorder       PromptBorder `json:\"\" yaml:\"border\"`\n\t}\n\n\t// PromptBorder tracks the color of the prompt depending on its kind (e.g., command or filter)\n\tPromptBorder struct {\n\t\tCommandColor Color `json:\"command\" yaml:\"command\"`\n\t\tDefaultColor Color `json:\"default\" yaml:\"default\"`\n\t}\n\n\t// Help tracks help styles.\n\tHelp struct {\n\t\tFgColor      Color `json:\"fgColor\" yaml:\"fgColor\"`\n\t\tBgColor      Color `json:\"bgColor\" yaml:\"bgColor\"`\n\t\tSectionColor Color `json:\"sectionColor\" yaml:\"sectionColor\"`\n\t\tKeyColor     Color `json:\"keyColor\" yaml:\"keyColor\"`\n\t\tNumKeyColor  Color `json:\"numKeyColor\" yaml:\"numKeyColor\"`\n\t}\n\n\t// Body tracks body styles.\n\tBody struct {\n\t\tFgColor        Color `json:\"fgColor\" yaml:\"fgColor\"`\n\t\tBgColor        Color `json:\"bgColor\" yaml:\"bgColor\"`\n\t\tLogoColor      Color `json:\"logoColor\" yaml:\"logoColor\"`\n\t\tLogoColorMsg   Color `json:\"logoColorMsg\" yaml:\"logoColorMsg\"`\n\t\tLogoColorInfo  Color `json:\"logoColorInfo\" yaml:\"logoColorInfo\"`\n\t\tLogoColorWarn  Color `json:\"logoColorWarn\" yaml:\"logoColorWarn\"`\n\t\tLogoColorError Color `json:\"logoColorError\" yaml:\"logoColorError\"`\n\t}\n\n\t// Dialog tracks dialog styles.\n\tDialog struct {\n\t\tFgColor            Color `json:\"fgColor\" yaml:\"fgColor\"`\n\t\tBgColor            Color `json:\"bgColor\" yaml:\"bgColor\"`\n\t\tButtonFgColor      Color `json:\"buttonFgColor\" yaml:\"buttonFgColor\"`\n\t\tButtonBgColor      Color `json:\"buttonBgColor\" yaml:\"buttonBgColor\"`\n\t\tButtonFocusFgColor Color `json:\"buttonFocusFgColor\" yaml:\"buttonFocusFgColor\"`\n\t\tButtonFocusBgColor Color `json:\"buttonFocusBgColor\" yaml:\"buttonFocusBgColor\"`\n\t\tLabelFgColor       Color `json:\"labelFgColor\" yaml:\"labelFgColor\"`\n\t\tFieldFgColor       Color `json:\"fieldFgColor\" yaml:\"fieldFgColor\"`\n\t}\n\n\t// Frame tracks frame styles.\n\tFrame struct {\n\t\tTitle  Title  `json:\"title\" yaml:\"title\"`\n\t\tBorder Border `json:\"border\" yaml:\"border\"`\n\t\tMenu   Menu   `json:\"menu\" yaml:\"menu\"`\n\t\tCrumb  Crumb  `json:\"crumbs\" yaml:\"crumbs\"`\n\t\tStatus Status `json:\"status\" yaml:\"status\"`\n\t}\n\n\t// Views tracks individual view styles.\n\tViews struct {\n\t\tTable  Table  `json:\"table\" yaml:\"table\"`\n\t\tXray   Xray   `json:\"xray\" yaml:\"xray\"`\n\t\tCharts Charts `json:\"charts\" yaml:\"charts\"`\n\t\tYaml   Yaml   `json:\"yaml\" yaml:\"yaml\"`\n\t\tPicker Picker `json:\"picker\" yaml:\"picker\"`\n\t\tLog    Log    `json:\"logs\" yaml:\"logs\"`\n\t}\n\n\t// Status tracks resource status styles.\n\tStatus struct {\n\t\tNewColor       Color `json:\"newColor\" yaml:\"newColor\"`\n\t\tModifyColor    Color `json:\"modifyColor\" yaml:\"modifyColor\"`\n\t\tAddColor       Color `json:\"addColor\" yaml:\"addColor\"`\n\t\tPendingColor   Color `json:\"pendingColor\" yaml:\"pendingColor\"`\n\t\tErrorColor     Color `json:\"errorColor\" yaml:\"errorColor\"`\n\t\tHighlightColor Color `json:\"highlightColor\" yaml:\"highlightColor\"`\n\t\tKillColor      Color `json:\"killColor\" yaml:\"killColor\"`\n\t\tCompletedColor Color `json:\"completedColor\" yaml:\"completedColor\"`\n\t}\n\n\t// Log tracks Log styles.\n\tLog struct {\n\t\tFgColor   Color        `json:\"fgColor\" yaml:\"fgColor\"`\n\t\tBgColor   Color        `json:\"bgColor\" yaml:\"bgColor\"`\n\t\tIndicator LogIndicator `json:\"indicator\" yaml:\"indicator\"`\n\t}\n\n\t// Picker tracks color when selecting containers\n\tPicker struct {\n\t\tMainColor     Color `json:\"mainColor\" yaml:\"mainColor\"`\n\t\tFocusColor    Color `json:\"focusColor\" yaml:\"focusColor\"`\n\t\tShortcutColor Color `json:\"shortcutColor\" yaml:\"shortcutColor\"`\n\t}\n\n\t// LogIndicator tracks log view indicator.\n\tLogIndicator struct {\n\t\tFgColor        Color `json:\"fgColor\" yaml:\"fgColor\"`\n\t\tBgColor        Color `json:\"bgColor\" yaml:\"bgColor\"`\n\t\tToggleOnColor  Color `json:\"toggleOnColor\" yaml:\"toggleOnColor\"`\n\t\tToggleOffColor Color `json:\"toggleOffColor\" yaml:\"toggleOffColor\"`\n\t}\n\n\t// Yaml tracks yaml styles.\n\tYaml struct {\n\t\tKeyColor   Color `json:\"keyColor\" yaml:\"keyColor\"`\n\t\tValueColor Color `json:\"valueColor\" yaml:\"valueColor\"`\n\t\tColonColor Color `json:\"colonColor\" yaml:\"colonColor\"`\n\t}\n\n\t// Title tracks title styles.\n\tTitle struct {\n\t\tFgColor        Color `json:\"fgColor\" yaml:\"fgColor\"`\n\t\tBgColor        Color `json:\"bgColor\" yaml:\"bgColor\"`\n\t\tHighlightColor Color `json:\"highlightColor\" yaml:\"highlightColor\"`\n\t\tCounterColor   Color `json:\"counterColor\" yaml:\"counterColor\"`\n\t\tFilterColor    Color `json:\"filterColor\" yaml:\"filterColor\"`\n\t}\n\n\t// Info tracks info styles.\n\tInfo struct {\n\t\tSectionColor Color `json:\"sectionColor\" yaml:\"sectionColor\"`\n\t\tFgColor      Color `json:\"fgColor\" yaml:\"fgColor\"`\n\t\tCPUColor     Color `json:\"cpuColor\" yaml:\"cpuColor\"`\n\t\tMEMColor     Color `json:\"memColor\" yaml:\"memColor\"`\n\t\tK9sRevColor  Color `json:\"k9sRevColor\" yaml:\"k9sRevColor\"`\n\t}\n\n\t// Border tracks border styles.\n\tBorder struct {\n\t\tFgColor    Color `json:\"fgColor\" yaml:\"fgColor\"`\n\t\tFocusColor Color `json:\"focusColor\" yaml:\"focusColor\"`\n\t}\n\n\t// Crumb tracks crumbs styles.\n\tCrumb struct {\n\t\tFgColor     Color `json:\"fgColor\" yaml:\"fgColor\"`\n\t\tBgColor     Color `json:\"bgColor\" yaml:\"bgColor\"`\n\t\tActiveColor Color `json:\"activeColor\" yaml:\"activeColor\"`\n\t}\n\n\t// Table tracks table styles.\n\tTable struct {\n\t\tFgColor       Color       `json:\"fgColor\" yaml:\"fgColor\"`\n\t\tBgColor       Color       `json:\"bgColor\" yaml:\"bgColor\"`\n\t\tCursorFgColor Color       `json:\"cursorFgColor\" yaml:\"cursorFgColor\"`\n\t\tCursorBgColor Color       `json:\"cursorBgColor\" yaml:\"cursorBgColor\"`\n\t\tMarkColor     Color       `json:\"markColor\" yaml:\"markColor\"`\n\t\tHeader        TableHeader `json:\"header\" yaml:\"header\"`\n\t}\n\n\t// TableHeader tracks table header styles.\n\tTableHeader struct {\n\t\tFgColor                 Color `json:\"fgColor\" yaml:\"fgColor\"`\n\t\tBgColor                 Color `json:\"bgColor\" yaml:\"bgColor\"`\n\t\tSorterColor             Color `json:\"sorterColor\" yaml:\"sorterColor\"`\n\t\tSelectedSortColumnColor Color `json:\"selectedSortColumnColor\" yaml:\"selectedSortColumnColor\"`\n\t}\n\n\t// Xray tracks xray styles.\n\tXray struct {\n\t\tFgColor         Color `json:\"fgColor\" yaml:\"fgColor\"`\n\t\tBgColor         Color `json:\"bgColor\" yaml:\"bgColor\"`\n\t\tCursorColor     Color `json:\"cursorColor\" yaml:\"cursorColor\"`\n\t\tCursorTextColor Color `json:\"cursorTextColor\" yaml:\"cursorTextColor\"`\n\t\tGraphicColor    Color `json:\"graphicColor\" yaml:\"graphicColor\"`\n\t}\n\n\t// Menu tracks menu styles.\n\tMenu struct {\n\t\tFgColor     Color     `json:\"fgColor\" yaml:\"fgColor\"`\n\t\tFgStyle     TextStyle `json:\"fgStyle\" yaml:\"fgStyle\"`\n\t\tKeyColor    Color     `json:\"keyColor\" yaml:\"keyColor\"`\n\t\tNumKeyColor Color     `json:\"numKeyColor\" yaml:\"numKeyColor\"`\n\t}\n\n\t// Charts tracks charts styles.\n\tCharts struct {\n\t\tBgColor            Color             `json:\"bgColor\" yaml:\"bgColor\"`\n\t\tDialBgColor        Color             `json:\"dialBgColor\" yaml:\"dialBgColor\"`\n\t\tChartBgColor       Color             `json:\"chartBgColor\" yaml:\"chartBgColor\"`\n\t\tDefaultDialColors  Colors            `json:\"defaultDialColors\" yaml:\"defaultDialColors\"`\n\t\tDefaultChartColors Colors            `json:\"defaultChartColors\" yaml:\"defaultChartColors\"`\n\t\tResourceColors     map[string]Colors `json:\"resourceColors\" yaml:\"resourceColors\"`\n\t\tFocusFgColor       Color             `yaml:\"focusFgColor\"`\n\t\tFocusBgColor       Color             `yaml:\"focusBgColor\"`\n\t}\n)\n\nfunc newStyle() Style {\n\treturn Style{\n\t\tBody:   newBody(),\n\t\tPrompt: newPrompt(),\n\t\tHelp:   newHelp(),\n\t\tFrame:  newFrame(),\n\t\tInfo:   newInfo(),\n\t\tViews:  newViews(),\n\t\tDialog: newDialog(),\n\t}\n}\n\nfunc newDialog() Dialog {\n\treturn Dialog{\n\t\tFgColor:            \"cadetblue\",\n\t\tBgColor:            \"black\",\n\t\tButtonBgColor:      \"darkslateblue\",\n\t\tButtonFgColor:      \"black\",\n\t\tButtonFocusBgColor: \"dodgerblue\",\n\t\tButtonFocusFgColor: \"black\",\n\t\tLabelFgColor:       \"white\",\n\t\tFieldFgColor:       \"white\",\n\t}\n}\n\nfunc newPrompt() Prompt {\n\treturn Prompt{\n\t\tFgColor:      \"cadetblue\",\n\t\tBgColor:      \"black\",\n\t\tSuggestColor: \"dodgerblue\",\n\t\tBorder: PromptBorder{\n\t\t\tDefaultColor: \"seagreen\",\n\t\t\tCommandColor: \"aqua\",\n\t\t},\n\t}\n}\n\nfunc newCharts() Charts {\n\treturn Charts{\n\t\tBgColor:            \"black\",\n\t\tDialBgColor:        \"black\",\n\t\tChartBgColor:       \"black\",\n\t\tDefaultDialColors:  Colors{Color(\"palegreen\"), Color(\"orangered\")},\n\t\tDefaultChartColors: Colors{Color(\"palegreen\"), Color(\"orangered\")},\n\t\tResourceColors: map[string]Colors{\n\t\t\tCPU: {Color(\"dodgerblue\"), Color(\"darkslateblue\")},\n\t\t\tMEM: {Color(\"yellow\"), Color(\"goldenrod\")},\n\t\t},\n\t\tFocusFgColor: \"white\",\n\t\tFocusBgColor: \"orange\",\n\t}\n}\n\nfunc newViews() Views {\n\treturn Views{\n\t\tTable:  newTable(),\n\t\tXray:   newXray(),\n\t\tCharts: newCharts(),\n\t\tYaml:   newYaml(),\n\t\tPicker: newPicker(),\n\t\tLog:    newLog(),\n\t}\n}\n\nfunc newFrame() Frame {\n\treturn Frame{\n\t\tTitle:  newTitle(),\n\t\tBorder: newBorder(),\n\t\tMenu:   newMenu(),\n\t\tCrumb:  newCrumb(),\n\t\tStatus: newStatus(),\n\t}\n}\n\nfunc newHelp() Help {\n\treturn Help{\n\t\tFgColor:      \"cadetblue\",\n\t\tBgColor:      \"black\",\n\t\tSectionColor: \"green\",\n\t\tKeyColor:     \"dodgerblue\",\n\t\tNumKeyColor:  \"fuchsia\",\n\t}\n}\n\nfunc newBody() Body {\n\treturn Body{\n\t\tFgColor:        \"cadetblue\",\n\t\tBgColor:        \"black\",\n\t\tLogoColor:      \"orange\",\n\t\tLogoColorMsg:   \"white\",\n\t\tLogoColorInfo:  \"green\",\n\t\tLogoColorWarn:  \"mediumvioletred\",\n\t\tLogoColorError: \"red\",\n\t}\n}\n\nfunc newStatus() Status {\n\treturn Status{\n\t\tNewColor:       \"lightskyblue\",\n\t\tModifyColor:    \"greenyellow\",\n\t\tAddColor:       \"dodgerblue\",\n\t\tPendingColor:   \"darkorange\",\n\t\tErrorColor:     \"orangered\",\n\t\tHighlightColor: \"aqua\",\n\t\tKillColor:      \"mediumpurple\",\n\t\tCompletedColor: \"lightslategray\",\n\t}\n}\n\nfunc newPicker() Picker {\n\treturn Picker{\n\t\tMainColor:     \"white\",\n\t\tFocusColor:    \"aqua\",\n\t\tShortcutColor: \"aqua\",\n\t}\n}\n\nfunc newLog() Log {\n\treturn Log{\n\t\tFgColor:   \"lightskyblue\",\n\t\tBgColor:   \"black\",\n\t\tIndicator: newLogIndicator(),\n\t}\n}\n\nfunc newLogIndicator() LogIndicator {\n\treturn LogIndicator{\n\t\tFgColor:        \"dodgerblue\",\n\t\tBgColor:        \"black\",\n\t\tToggleOnColor:  \"limegreen\",\n\t\tToggleOffColor: \"gray\",\n\t}\n}\n\nfunc newYaml() Yaml {\n\treturn Yaml{\n\t\tKeyColor:   \"steelblue\",\n\t\tColonColor: \"white\",\n\t\tValueColor: \"papayawhip\",\n\t}\n}\n\nfunc newTitle() Title {\n\treturn Title{\n\t\tFgColor:        \"aqua\",\n\t\tBgColor:        \"black\",\n\t\tHighlightColor: \"fuchsia\",\n\t\tCounterColor:   \"papayawhip\",\n\t\tFilterColor:    \"seagreen\",\n\t}\n}\n\nfunc newInfo() Info {\n\treturn Info{\n\t\tSectionColor: \"white\",\n\t\tFgColor:      \"orange\",\n\t\tCPUColor:     \"lawngreen\",\n\t\tMEMColor:     \"darkturquoise\",\n\t\tK9sRevColor:  \"aqua\",\n\t}\n}\n\nfunc newXray() Xray {\n\treturn Xray{\n\t\tFgColor:         \"aqua\",\n\t\tBgColor:         \"black\",\n\t\tCursorColor:     \"dodgerblue\",\n\t\tCursorTextColor: \"black\",\n\t\tGraphicColor:    \"cadetblue\",\n\t}\n}\n\nfunc newTable() Table {\n\treturn Table{\n\t\tFgColor:       \"aqua\",\n\t\tBgColor:       \"black\",\n\t\tCursorFgColor: \"black\",\n\t\tCursorBgColor: \"aqua\",\n\t\tMarkColor:     \"palegreen\",\n\t\tHeader:        newTableHeader(),\n\t}\n}\n\nfunc newTableHeader() TableHeader {\n\treturn TableHeader{\n\t\tFgColor:                 \"white\",\n\t\tBgColor:                 \"black\",\n\t\tSorterColor:             \"aqua\",\n\t\tSelectedSortColumnColor: \"lightskyblue\",\n\t}\n}\n\nfunc newCrumb() Crumb {\n\treturn Crumb{\n\t\tFgColor:     \"black\",\n\t\tBgColor:     \"aqua\",\n\t\tActiveColor: \"orange\",\n\t}\n}\n\nfunc newBorder() Border {\n\treturn Border{\n\t\tFgColor:    \"dodgerblue\",\n\t\tFocusColor: \"lightskyblue\",\n\t}\n}\n\nfunc newMenu() Menu {\n\treturn Menu{\n\t\tFgColor:     \"white\",\n\t\tKeyColor:    \"dodgerblue\",\n\t\tNumKeyColor: \"fuchsia\",\n\t}\n}\n\n// NewStyles creates a new default config.\nfunc NewStyles() *Styles {\n\tvar s Styles\n\tif err := yaml.Unmarshal(stockSkinTpl, &s); err == nil {\n\t\treturn &s\n\t}\n\n\treturn &Styles{\n\t\tK9s: newStyle(),\n\t}\n}\n\n// Reset resets styles.\nfunc (s *Styles) Reset(invert bool) {\n\tif err := yaml.Unmarshal(stockSkinTpl, s); err != nil {\n\t\ts.K9s = newStyle()\n\t}\n\tif invert {\n\t\ts.K9s.Invert()\n\t}\n}\n\n// FgColor returns the foreground color.\nfunc (s *Styles) FgColor() tcell.Color {\n\treturn s.Body().FgColor.Color()\n}\n\n// BgColor returns the background color.\nfunc (s *Styles) BgColor() tcell.Color {\n\treturn s.Body().BgColor.Color()\n}\n\n// AddListener registers a new listener.\nfunc (s *Styles) AddListener(l StyleListener) {\n\ts.listeners = append(s.listeners, l)\n}\n\n// RemoveListener removes a listener.\nfunc (s *Styles) RemoveListener(l StyleListener) {\n\tvictim := -1\n\tfor i, lis := range s.listeners {\n\t\tif lis == l {\n\t\t\tvictim = i\n\t\t\tbreak\n\t\t}\n\t}\n\tif victim == -1 {\n\t\treturn\n\t}\n\ts.listeners = append(s.listeners[:victim], s.listeners[victim+1:]...)\n}\n\nfunc (s *Styles) fireStylesChanged() {\n\tfor _, list := range s.listeners {\n\t\tlist.StylesChanged(s)\n\t}\n}\n\n// Body returns body styles.\nfunc (s *Styles) Body() Body {\n\treturn s.K9s.Body\n}\n\n// Prompt returns prompt styles.\nfunc (s *Styles) Prompt() Prompt {\n\treturn s.K9s.Prompt\n}\n\n// Frame returns frame styles.\nfunc (s *Styles) Frame() Frame {\n\treturn s.K9s.Frame\n}\n\n// Crumb returns crumb styles.\nfunc (s *Styles) Crumb() Crumb {\n\treturn s.Frame().Crumb\n}\n\n// Title returns title styles.\nfunc (s *Styles) Title() Title {\n\treturn s.Frame().Title\n}\n\n// Charts returns charts styles.\nfunc (s *Styles) Charts() Charts {\n\treturn s.K9s.Views.Charts\n}\n\n// Dialog returns dialog styles.\nfunc (s *Styles) Dialog() Dialog {\n\treturn s.K9s.Dialog\n}\n\n// Table returns table styles.\nfunc (s *Styles) Table() Table {\n\treturn s.K9s.Views.Table\n}\n\n// Xray returns xray styles.\nfunc (s *Styles) Xray() Xray {\n\treturn s.K9s.Views.Xray\n}\n\n// Views returns views styles.\nfunc (s *Styles) Views() Views {\n\treturn s.K9s.Views\n}\n\n// Invert inverts all colors in the Style.\nfunc (s *Style) Invert() {\n\ts.Body.Invert()\n\ts.Prompt.Invert()\n\ts.Help.Invert()\n\ts.Frame.Invert()\n\ts.Info.Invert()\n\ts.Views.Invert()\n\ts.Dialog.Invert()\n}\n\n// Invert inverts all colors in Body.\nfunc (b *Body) Invert() {\n\tb.FgColor = b.FgColor.InvertColor()\n\tb.BgColor = b.BgColor.InvertColor()\n\tb.LogoColor = b.LogoColor.InvertColor()\n\tb.LogoColorMsg = b.LogoColorMsg.InvertColor()\n\tb.LogoColorInfo = b.LogoColorInfo.InvertColor()\n\tb.LogoColorWarn = b.LogoColorWarn.InvertColor()\n\tb.LogoColorError = b.LogoColorError.InvertColor()\n}\n\n// Invert inverts all colors in Prompt.\nfunc (p *Prompt) Invert() {\n\tp.FgColor = p.FgColor.InvertColor()\n\tp.BgColor = p.BgColor.InvertColor()\n\tp.SuggestColor = p.SuggestColor.InvertColor()\n\tp.Border.Invert()\n}\n\n// Invert inverts all colors in PromptBorder.\nfunc (p *PromptBorder) Invert() {\n\tp.CommandColor = p.CommandColor.InvertColor()\n\tp.DefaultColor = p.DefaultColor.InvertColor()\n}\n\n// Invert inverts all colors in Help.\nfunc (h *Help) Invert() {\n\th.FgColor = h.FgColor.InvertColor()\n\th.BgColor = h.BgColor.InvertColor()\n\th.SectionColor = h.SectionColor.InvertColor()\n\th.KeyColor = h.KeyColor.InvertColor()\n\th.NumKeyColor = h.NumKeyColor.InvertColor()\n}\n\n// Invert inverts all colors in Dialog.\nfunc (d *Dialog) Invert() {\n\td.FgColor = d.FgColor.InvertColor()\n\td.BgColor = d.BgColor.InvertColor()\n\td.ButtonFgColor = d.ButtonFgColor.InvertColor()\n\td.ButtonBgColor = d.ButtonBgColor.InvertColor()\n\td.ButtonFocusFgColor = d.ButtonFocusFgColor.InvertColor()\n\td.ButtonFocusBgColor = d.ButtonFocusBgColor.InvertColor()\n\td.LabelFgColor = d.LabelFgColor.InvertColor()\n\td.FieldFgColor = d.FieldFgColor.InvertColor()\n}\n\n// Invert inverts all colors in Frame.\nfunc (f *Frame) Invert() {\n\tf.Title.Invert()\n\tf.Border.Invert()\n\tf.Menu.Invert()\n\tf.Crumb.Invert()\n\tf.Status.Invert()\n}\n\n// Invert inverts all colors in Title.\nfunc (t *Title) Invert() {\n\tt.FgColor = t.FgColor.InvertColor()\n\tt.BgColor = t.BgColor.InvertColor()\n\tt.HighlightColor = t.HighlightColor.InvertColor()\n\tt.CounterColor = t.CounterColor.InvertColor()\n\tt.FilterColor = t.FilterColor.InvertColor()\n}\n\n// Invert inverts all colors in Border.\nfunc (b *Border) Invert() {\n\tb.FgColor = b.FgColor.InvertColor()\n\tb.FocusColor = b.FocusColor.InvertColor()\n}\n\n// Invert inverts all colors in Menu.\nfunc (m *Menu) Invert() {\n\tm.FgColor = m.FgColor.InvertColor()\n\tm.KeyColor = m.KeyColor.InvertColor()\n\tm.NumKeyColor = m.NumKeyColor.InvertColor()\n}\n\n// Invert inverts all colors in Crumb.\nfunc (c *Crumb) Invert() {\n\tc.FgColor = c.FgColor.InvertColor()\n\tc.BgColor = c.BgColor.InvertColor()\n\tc.ActiveColor = c.ActiveColor.InvertColor()\n}\n\n// Invert inverts all colors in Status.\nfunc (s *Status) Invert() {\n\ts.NewColor = s.NewColor.InvertColor()\n\ts.ModifyColor = s.ModifyColor.InvertColor()\n\ts.AddColor = s.AddColor.InvertColor()\n\ts.PendingColor = s.PendingColor.InvertColor()\n\ts.ErrorColor = s.ErrorColor.InvertColor()\n\ts.HighlightColor = s.HighlightColor.InvertColor()\n\ts.KillColor = s.KillColor.InvertColor()\n\ts.CompletedColor = s.CompletedColor.InvertColor()\n}\n\n// Invert inverts all colors in Info.\nfunc (i *Info) Invert() {\n\ti.SectionColor = i.SectionColor.InvertColor()\n\ti.FgColor = i.FgColor.InvertColor()\n\ti.CPUColor = i.CPUColor.InvertColor()\n\ti.MEMColor = i.MEMColor.InvertColor()\n\ti.K9sRevColor = i.K9sRevColor.InvertColor()\n}\n\n// Invert inverts all colors in Views.\nfunc (v *Views) Invert() {\n\tv.Table.Invert()\n\tv.Xray.Invert()\n\tv.Charts.Invert()\n\tv.Yaml.Invert()\n\tv.Picker.Invert()\n\tv.Log.Invert()\n}\n\n// Invert inverts all colors in Table.\nfunc (t *Table) Invert() {\n\tt.FgColor = t.FgColor.InvertColor()\n\tt.BgColor = t.BgColor.InvertColor()\n\tt.CursorFgColor = t.CursorFgColor.InvertColor()\n\tt.CursorBgColor = t.CursorBgColor.InvertColor()\n\tt.MarkColor = t.MarkColor.InvertColor()\n\tt.Header.Invert()\n}\n\n// Invert inverts all colors in TableHeader.\nfunc (t *TableHeader) Invert() {\n\tt.FgColor = t.FgColor.InvertColor()\n\tt.BgColor = t.BgColor.InvertColor()\n\tt.SorterColor = t.SorterColor.InvertColor()\n\tt.SelectedSortColumnColor = t.SelectedSortColumnColor.InvertColor()\n}\n\n// Invert inverts all colors in Xray.\nfunc (x *Xray) Invert() {\n\tx.FgColor = x.FgColor.InvertColor()\n\tx.BgColor = x.BgColor.InvertColor()\n\tx.CursorColor = x.CursorColor.InvertColor()\n\tx.CursorTextColor = x.CursorTextColor.InvertColor()\n\tx.GraphicColor = x.GraphicColor.InvertColor()\n}\n\n// Invert inverts all colors in Charts.\nfunc (c *Charts) Invert() {\n\tc.BgColor = c.BgColor.InvertColor()\n\tc.DialBgColor = c.DialBgColor.InvertColor()\n\tc.ChartBgColor = c.ChartBgColor.InvertColor()\n\tc.FocusFgColor = c.FocusFgColor.InvertColor()\n\tc.FocusBgColor = c.FocusBgColor.InvertColor()\n\tc.DefaultDialColors = c.DefaultDialColors.Invert()\n\tc.DefaultChartColors = c.DefaultChartColors.Invert()\n\tfor k, v := range c.ResourceColors {\n\t\tc.ResourceColors[k] = v.Invert()\n\t}\n}\n\n// Invert inverts all colors in Yaml.\nfunc (y *Yaml) Invert() {\n\ty.KeyColor = y.KeyColor.InvertColor()\n\ty.ValueColor = y.ValueColor.InvertColor()\n\ty.ColonColor = y.ColonColor.InvertColor()\n}\n\n// Invert inverts all colors in Picker.\nfunc (p *Picker) Invert() {\n\tp.MainColor = p.MainColor.InvertColor()\n\tp.FocusColor = p.FocusColor.InvertColor()\n\tp.ShortcutColor = p.ShortcutColor.InvertColor()\n}\n\n// Invert inverts all colors in Log.\nfunc (l *Log) Invert() {\n\tl.FgColor = l.FgColor.InvertColor()\n\tl.BgColor = l.BgColor.InvertColor()\n\tl.Indicator.Invert()\n}\n\n// Invert inverts all colors in LogIndicator.\nfunc (l *LogIndicator) Invert() {\n\tl.FgColor = l.FgColor.InvertColor()\n\tl.BgColor = l.BgColor.InvertColor()\n\tl.ToggleOnColor = l.ToggleOnColor.InvertColor()\n\tl.ToggleOffColor = l.ToggleOffColor.InvertColor()\n}\n\n// Load K9s configuration from file.\nfunc (s *Styles) Load(path string, invert bool) error {\n\tbb, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := data.JSONValidator.Validate(json.SkinSchema, bb); err != nil {\n\t\treturn err\n\t}\n\tif err := yaml.Unmarshal(bb, s); err != nil {\n\t\treturn err\n\t}\n\n\tif invert {\n\t\ts.K9s.Invert()\n\t}\n\n\treturn nil\n}\n\n// Update apply terminal colors based on styles.\nfunc (s *Styles) Update() {\n\ttview.Styles.PrimitiveBackgroundColor = s.BgColor()\n\ttview.Styles.ContrastBackgroundColor = s.BgColor()\n\ttview.Styles.MoreContrastBackgroundColor = s.BgColor()\n\ttview.Styles.PrimaryTextColor = s.FgColor()\n\ttview.Styles.BorderColor = s.K9s.Frame.Border.FgColor.Color()\n\ttview.Styles.FocusColor = s.K9s.Frame.Border.FocusColor.Color()\n\ttview.Styles.TitleColor = s.FgColor()\n\ttview.Styles.GraphicsColor = s.FgColor()\n\ttview.Styles.SecondaryTextColor = s.FgColor()\n\ttview.Styles.TertiaryTextColor = s.FgColor()\n\ttview.Styles.InverseTextColor = s.FgColor()\n\ttview.Styles.ContrastSecondaryTextColor = s.FgColor()\n\n\ts.fireStylesChanged()\n}\n\n// Dump for debug.\nfunc (s *Styles) Dump() {\n\tbb, _ := yaml.Marshal(s)\n\tfmt.Println(string(bb))\n}\n"
  },
  {
    "path": "internal/config/styles_int_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_newStyle(t *testing.T) {\n\ts := newStyle()\n\n\tassert.Equal(t, Color(\"black\"), s.Body.BgColor)\n\tassert.Equal(t, Color(\"cadetblue\"), s.Body.FgColor)\n\tassert.Equal(t, Color(\"lightskyblue\"), s.Frame.Status.NewColor)\n}\n"
  },
  {
    "path": "internal/config/styles_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewStyle(t *testing.T) {\n\ts := config.NewStyles()\n\n\tassert.Equal(t, config.Color(\"black\"), s.K9s.Body.BgColor)\n\tassert.Equal(t, config.Color(\"cadetblue\"), s.K9s.Body.FgColor)\n\tassert.Equal(t, config.Color(\"lightskyblue\"), s.K9s.Frame.Status.NewColor)\n}\n\nfunc TestColor(t *testing.T) {\n\tuu := map[string]tcell.Color{\n\t\t\"blah\":    tcell.ColorDefault,\n\t\t\"blue\":    tcell.ColorBlue.TrueColor(),\n\t\t\"#ffffff\": tcell.NewHexColor(16777215),\n\t\t\"#ff0000\": tcell.NewHexColor(16711680),\n\t}\n\n\tfor k := range uu {\n\t\tc, u := k, uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u, config.NewColor(c).Color())\n\t\t})\n\t}\n}\n\nfunc TestSkinHappy(t *testing.T) {\n\ts := config.NewStyles()\n\trequire.NoError(t, s.Load(\"../../skins/black-and-wtf.yaml\", false))\n\ts.Update()\n\n\tassert.Equal(t, \"#ffffff\", s.Body().FgColor.String())\n\tassert.Equal(t, \"#000000\", s.Body().BgColor.String())\n\tassert.Equal(t, \"#000000\", s.Table().BgColor.String())\n\tassert.Equal(t, tcell.ColorWhite.TrueColor(), s.FgColor())\n\tassert.Equal(t, tcell.ColorBlack.TrueColor(), s.BgColor())\n\tassert.Equal(t, tcell.ColorBlack.TrueColor(), tview.Styles.PrimitiveBackgroundColor)\n}\n\nfunc TestSkinLoad(t *testing.T) {\n\tuu := map[string]struct {\n\t\tf   string\n\t\terr string\n\t}{\n\t\t\"not-exist\": {\n\t\t\tf:   \"testdata/skins/blee.yaml\",\n\t\t\terr: \"open testdata/skins/blee.yaml: no such file or directory\",\n\t\t},\n\t\t\"toast\": {\n\t\t\tf: \"testdata/skins/boarked.yaml\",\n\t\t\terr: `Additional property bgColor is not allowed\nAdditional property fgColor is not allowed\nAdditional property logoColor is not allowed\nInvalid type. Expected: object, given: array`,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\ts := config.NewStyles()\n\t\t\terr := s.Load(u.f, false)\n\t\t\tif err != nil {\n\t\t\t\tassert.Equal(t, u.err, err.Error())\n\t\t\t}\n\t\t\tassert.Equal(t, \"#5f9ea0\", s.Body().FgColor.String())\n\t\t\tassert.Equal(t, \"#000000\", s.Body().BgColor.String())\n\t\t\tassert.Equal(t, \"#000000\", s.Table().BgColor.String())\n\t\t\tassert.Equal(t, tcell.ColorCadetBlue.TrueColor(), s.FgColor())\n\t\t\tassert.Equal(t, tcell.ColorBlack.TrueColor(), s.BgColor())\n\t\t\tassert.Equal(t, tcell.ColorBlack.TrueColor(), tview.Styles.PrimitiveBackgroundColor)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/config/templates/aliases.yaml",
    "content": "aliases:\n  dp: deployments\n  sec: v1/secrets\n  jo: jobs\n  cr: clusterroles\n  crb: clusterrolebindings\n  ro: roles\n  rb: rolebindings\n  np: networkpolicies\n"
  },
  {
    "path": "internal/config/templates/benchmarks.yaml",
    "content": "benchmarks:\n  defaults:\n    concurrency: 2\n    requests: 200"
  },
  {
    "path": "internal/config/templates/hotkeys.yaml",
    "content": "hotKeys:\n  # Examples...\n  # shift-0:\n  #   shortCut: Shift-0\n  #   description: View Workloads\n  #   command: wk k8s-app=cilium"
  },
  {
    "path": "internal/config/templates/stock-skin.yaml",
    "content": "# -----------------------------------------------------------------------------\n# Stock skin\n# -----------------------------------------------------------------------------\n\n# Skin...\nk9s:\n  body:\n    fgColor: cadetblue\n    bgColor: black\n    logoColor: orange\n    logoColorMsg: white\n    logoColorInfo: green\n    logoColorWarn: mediumvioletred\n    logoColorError: red\n  prompt:\n    fgColor: cadetblue\n    bgColor: black\n    suggestColor: dodgerblue\n    border:\n      command: aqua\n      default: seagreen\n  help:\n    fgColor: cadetblue\n    bgColor: black\n    sectionColor: green\n    keyColor: dodgerblue\n    numKeyColor: fuchsia\n  frame:\n    title:\n      fgColor: aqua\n      bgColor: black\n      highlightColor: fuchsia\n      counterColor: papayawhip\n      filterColor: seagreen\n    border:\n      fgColor: dodgerblue\n      focusColor: lightskyblue\n    menu:\n      fgColor: white\n      keyColor: dodgerblue\n      numKeyColor: fuchsia\n    crumbs:\n      fgColor: black\n      bgColor: aqua\n      activeColor: orange\n    status:\n      newColor: lightskyblue\n      modifyColor: greenyellow\n      addColor: dodgerblue\n      pendingColor: darkorange\n      errorColor: orangered\n      highlightColor: aqua\n      killColor: mediumpurple\n      completedColor: lightslategray\n  info:\n    sectionColor: white\n    fgColor: orange\n  views:\n    table:\n      fgColor: aqua\n      bgColor: black\n      cursorFgColor: black\n      cursorBgColor: aqua\n      markColor: palegreen\n      header:\n        fgColor: white\n        bgColor: black\n        sorterColor: aqua\n        selectedSortColumnColor: lightskyblue\n    xray:\n      fgColor: aqua\n      bgColor: black\n      cursorColor: dodgerblue\n      cursorTextColor: black\n      graphicColor: cadetblue\n    charts:\n      bgColor: black\n      dialBgColor: black\n      chartBgColor: black\n      focusFgColor: white\n      focusBgColor: orange\n      defaultDialColors:\n      - palegreen\n      - orangered\n      defaultChartColors:\n      - palegreen\n      - orangered\n      resourceColors:\n        cpu:\n        - dodgerblue\n        - darkslateblue\n        mem:\n        - yellow\n        - goldenrod\n    yaml:\n      keyColor: steelblue\n      valueColor: papayawhip\n      colonColor: white\n    picker:\n      mainColor: white\n      focusColor: aqua\n      shortcutColor: aqua\n    logs:\n      fgColor: lightskyblue\n      bgColor: black\n      indicator:\n        fgColor: dodgerblue\n        bgColor: black\n        toggleOnColor: limegreen\n        toggleOffColor: gray\n  dialog:\n    fgColor: cadetblue\n    bgColor: black\n    buttonFgColor: black\n    buttonBgColor: darkslateblue\n    buttonFocusFgColor: black\n    buttonFocusBgColor: dodgerblue\n    labelFgColor: white\n    fieldFgColor: white"
  },
  {
    "path": "internal/config/testdata/aliases/aliases.yaml",
    "content": "aliases:\n  dp: apps/v1/deployments\n  sec: v1/secrets\n  jo: batch/v1/jobs\n  cr: rbac.authorization.k8s.io/v1/clusterroles\n  crb: rbac.authorization.k8s.io/v1/clusterrolebindings\n  ro: rbac.authorization.k8s.io/v1/roles\n  rb: rbac.authorization.k8s.io/v1/rolebindings\n  np: networking.k8s.io/v1/networkpolicies\n"
  },
  {
    "path": "internal/config/testdata/aliases/plain.yaml",
    "content": "aliases:\n  dp: \"apps/v1/deployments\"\n  pe: \"v1/pods\"\n"
  },
  {
    "path": "internal/config/testdata/benchmarks/b_containers.yaml",
    "content": "benchmarks:\n  defaults:\n    concurrency: 2\n    requests: 1000\n  containers:\n    c1:\n      concurrency: 2\n      requests: 1000\n      http:\n        method: GET\n        http2: true\n        host: 10.10.10.10\n        path: /duh\n        body: |-\n          {\"fred\": \"blee\"}\n        headers:\n          Accept:\n            - text/html\n          Content-Type:\n            - application/json\n      auth:\n        user: \"fred\"\n        password: \"blee\"\n    c2:\n      concurrency: 10\n      requests: 1500\n      http:\n        method: POST\n        http2: false\n        host: 20.20.20.20\n        path: /fred\n        body: |-\n          {\"fred\": \"blee\"}\n        headers:\n          Accept:\n            - text/html\n          Content-Type:\n            - application/json\n      auth:\n        user: \"fred\"\n        password: \"blee\"\n  services:\n    default/nginx:\n      concurrency: 2\n      requests: 1000\n      http:\n        method: GET\n        http2: true\n        host: 10.10.10.10\n        path: /\n        body: |-\n          {\"fred\": \"blee\"}\n        headers:\n          Accept:\n            - text/html\n          Content-Type:\n            - application/json\n      auth:\n        user: \"fred\"\n        password: \"blee\"\n    blee/fred:\n      concurrency: 10\n      requests: 1500\n      http:\n        method: POST\n        http2: false\n        host: 20.20.20.20\n        path: /blee\n        body: |-\n          {\"fred\": \"blee\"}\n        headers:\n          Accept:\n            - text/html\n          Content-Type:\n            - application/json\n        auth:\n          user: \"fred\"\n          password: \"blee\""
  },
  {
    "path": "internal/config/testdata/benchmarks/b_containers_1.yaml",
    "content": "benchmarks:\n  defaults:\n    concurrency: 20\n    requests: 100\n  containers:\n    c1:\n      concurrency: 2\n      requests: 1000\n      http:\n        method: GET\n        http2: true\n        host: 10.10.10.10\n        path: /duh\n        body: |-\n          {\"fred\": \"blee\"}\n      headers:\n        Accept:\n          - text/html\n        Content-Type:\n          - application/json\n      auth:\n        user: \"fred\"\n        password: \"blee\"\n    c2:\n      concurrency: 10\n      requests: 1500\n      http:\n        method: POST\n        http2: false\n        host: 20.20.20.20\n        path: /fred\n        body: |-\n          {\"fred\": \"blee\"}\n        headers:\n          Accept:\n            - text/html\n          Content-Type:\n            - application/json\n      auth:\n        user: \"fred\"\n        password: \"blee\"\n  services:\n    default/nginx:\n      concurrency: 2\n      requests: 1000\n      http:\n        method: GET\n        http2: true\n        host: 10.10.10.10\n        path: /\n        body: |-\n          {\"fred\": \"blee\"}\n        headers:\n          Accept:\n            - text/html\n          Content-Type:\n            - application/json\n      auth:\n        user: \"fred\"\n        password: \"blee\"\n    blee/fred:\n      concurrency: 10\n      requests: 1500\n      http:\n        method: POST\n        http2: false\n        host: 20.20.20.20\n        path: /blee\n        body: |-\n          {\"fred\": \"blee\"}\n        headers:\n          Accept:\n            - text/html\n          Content-Type:\n            - application/json\n      auth:\n        user: \"fred\"\n        password: \"blee\"\n"
  },
  {
    "path": "internal/config/testdata/benchmarks/b_good.yaml",
    "content": "benchmarks:\n  defaults:\n    concurrency: 2\n    requests: 1000\n  services:\n    default/nginx:\n      concurrency: 2\n      requests: 1000\n      http:\n        method: GET\n        http2: true\n        host: 10.10.10.10\n        path: /\n        body: |-\n          {\"fred\": \"blee\"}\n        headers:\n          Accept:\n            - text/html\n          Content-Type:\n            - application/json\n      auth:\n        user: \"fred\"\n        password: \"blee\"\n    blee/fred:\n      concurrency: 10\n      requests: 1500\n      http:\n        method: POST\n        http2: false\n        host: 20.20.20.20\n        path: /zorg\n        body: |-\n          {\"fred\": \"blee\"}\n        headers:\n          Accept:\n            - text/html\n          Content-Type:\n            - application/json\n      auth:\n        user: \"fred\"\n        password: \"blee\"\n"
  },
  {
    "path": "internal/config/testdata/benchmarks/b_toast.yaml",
    "content": "benchmarks:\n  service:\n    - default/nginx:\n      concurrency: 1\n      http:\n        requests: 100\n        http2: true\n        method: GET\n        url: http://35.224.16.201/\n        body: |-\n          {\"fred\": \"blee\"}\n        headers:\n        - \"Accept: text/html\"\n        - \"Content-Type: application/json\"\n      auth:\n        user: \"fred\"\n        password: \"blee\""
  },
  {
    "path": "internal/config/testdata/benchmarks/bench-fred.yaml",
    "content": "benchmarks:\n  defaults:\n    concurrency: 2\n    requests: 1000\n  services:\n    default/nginx:\n      concurrency: 2\n      requests: 1000\n      http:\n        method: GET\n        http2: true\n        host: 10.10.10.10\n        path: /\n        body: |-\n          {\"fred\": \"blee\"}\n        headers:\n          Accept:\n            - text/html\n          Content-Type:\n            - application/json\n      auth:\n        user: \"fred\"\n        password: \"blee\"\n    blee/fred:\n      concurrency: 10\n      requests: 1500\n      http:\n        method: POST\n        http2: false\n        host: 20.20.20.20\n        path: /zorg\n        body: |-\n          {\"fred\": \"blee\"}\n        headers:\n          Accept:\n            - text/html\n          Content-Type:\n            - application/json\n      auth:\n        user: \"fred\"\n        password: \"blee\"\n"
  },
  {
    "path": "internal/config/testdata/configs/default.yaml",
    "content": "k9s:\n  liveViewAutoRefresh: false\n  gpuVendors: {}\n  screenDumpDir: /tmp/k9s-test/screen-dumps\n  refreshRate: 2\n  apiServerTimeout: 2m0s\n  maxConnRetry: 5\n  readOnly: false\n  noExitOnCtrlC: false\n  portForwardAddress: localhost\n  ui:\n    enableMouse: false\n    headless: false\n    logoless: false\n    crumbsless: false\n    splashless: false\n    reactive: false\n    noIcons: false\n    invert: false\n    defaultsToFullScreen: false\n    useFullGVRTitle: false\n  skipLatestRevCheck: false\n  disablePodCounting: false\n  shellPod:\n    image: busybox:1.37.0\n    namespace: default\n    limits:\n      cpu: 100m\n      memory: 100Mi\n  imageScans:\n    enable: false\n    exclusions:\n      namespaces: []\n      labels: {}\n  logger:\n    tail: 100\n    buffer: 5000\n    sinceSeconds: -1\n    textWrap: false\n    disableAutoscroll: false\n    columnLock: false\n    showTime: false\n  thresholds:\n    cpu:\n      critical: 90\n      warn: 70\n    memory:\n      critical: 90\n      warn: 70\n  defaultView: \"\"\n"
  },
  {
    "path": "internal/config/testdata/configs/expected.yaml",
    "content": "k9s:\n  liveViewAutoRefresh: true\n  gpuVendors:\n    bozo: bozo/gpu.com\n  screenDumpDir: /tmp/k9s-test/screen-dumps\n  refreshRate: 100\n  apiServerTimeout: 30s\n  maxConnRetry: 5\n  readOnly: true\n  noExitOnCtrlC: false\n  portForwardAddress: localhost\n  ui:\n    enableMouse: false\n    headless: false\n    logoless: false\n    crumbsless: false\n    splashless: false\n    reactive: false\n    noIcons: false\n    invert: false\n    defaultsToFullScreen: false\n    useFullGVRTitle: true\n  skipLatestRevCheck: false\n  disablePodCounting: false\n  shellPod:\n    image: busybox:1.37.0\n    namespace: default\n    limits:\n      cpu: 100m\n      memory: 100Mi\n  imageScans:\n    enable: false\n    exclusions:\n      namespaces: []\n      labels: {}\n  logger:\n    tail: 500\n    buffer: 800\n    sinceSeconds: -1\n    textWrap: false\n    disableAutoscroll: false\n    columnLock: false\n    showTime: false\n  thresholds:\n    cpu:\n      critical: 90\n      warn: 70\n    memory:\n      critical: 90\n      warn: 70\n  defaultView: \"\"\n"
  },
  {
    "path": "internal/config/testdata/configs/k9s.yaml",
    "content": "k9s:\n  liveViewAutoRefresh: true\n  gpuVendors: {}\n  screenDumpDir: /tmp/k9s-test/screen-dumps\n  refreshRate: 2\n  apiServerTimeout: 10s\n  maxConnRetry: 5\n  readOnly: false\n  noExitOnCtrlC: false\n  portForwardAddress: localhost\n  ui:\n    enableMouse: false\n    headless: false\n    logoless: false\n    crumbsless: false\n    splashless: false\n    reactive: false\n    noIcons: false\n    invert: false\n    defaultsToFullScreen: false\n    useFullGVRTitle: false\n  skipLatestRevCheck: false\n  disablePodCounting: false\n  shellPod:\n    image: busybox:1.37.0\n    namespace: default\n    limits:\n      cpu: 100m\n      memory: 100Mi\n  imageScans:\n    enable: false\n    exclusions:\n      namespaces: []\n      labels: {}\n  logger:\n    tail: 200\n    buffer: 2000\n    sinceSeconds: -1\n    textWrap: false\n    disableAutoscroll: false\n    columnLock: false\n    showTime: false\n  thresholds:\n    cpu:\n      critical: 90\n      warn: 70\n    memory:\n      critical: 90\n      warn: 70\n  defaultView: \"\"\n"
  },
  {
    "path": "internal/config/testdata/configs/k9s_toast.yaml",
    "content": "k9s:\n  liveViewAutoRefresh: true\n  screenDumpDir: /tmp/screen-dumps\n  refreshRate: 2\n  maxConnRetry: 5\n  readOnly: false\n  noExitOnCtrlC: false\n  ui:\n    enableMouse: false\n    headless: false\n    logoless: false\n    crumbsless: false\n    splashless: false\n    noIcons: false\n    invert: false\n  skipLatestRevCheck: yes\n  disablePodCounts: false\n  shellPods:\n    image: busybox:1.37.0\n    namespace: default\n    limits:\n      cpu: 100m\n      memory: 100Mi\n  imageScans:\n    enable: false\n    exclusions:\n      namespaces: []\n      labels: {}\n  logger:\n    tail: 200\n    buffer: 2000\n    sinceSeconds: -1\n    textWrap: false\n    disableAutoscroll: false\n    columnLock: false\n    showTime: false\n  thresholds:\n    cpu:\n      critical: 90\n      warn: 70\n    memory:\n      critical: 90\n      warn: 70\n  defaultView: \"\"\n"
  },
  {
    "path": "internal/config/testdata/hotkeys/hotkeys.yaml",
    "content": "hotKeys:\n  pods:\n    shortCut: shift-0\n    description: Launch pod view\n    command: pods\n    keepHistory: true\n"
  },
  {
    "path": "internal/config/testdata/k8s.yaml",
    "content": "apiVersion: v1\nkind: Config\npreferences: {}\nclusters:\n- cluster:\n    insecure-skip-tls-verify: true\n    server: https://localhost:6443\n  name: docker-for-desktop-cluster\ncontexts:\n- context:\n    cluster: docker-for-desktop-cluster\n    user: docker-for-desktop\n  name: docker-for-desktop\ncurrent-context: docker-for-desktop\nusers:\n- name: docker-for-desktop\n  user:\n    client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM5RENDQWR5Z0F3SUJBZ0lJWFNHb3I3ZlJlOHN3RFFZSktvWklodmNOQVFFTEJRQXdGVEVUTUJFR0ExVUUKQXhNS2EzVmlaWEp1WlhSbGN6QWVGdzB4T0RBNU1ESXlNREkyTlRaYUZ3MHlNREF5TURreE5qQXhNVGhhTURZeApGekFWQmdOVkJBb1REbk41YzNSbGJUcHRZWE4wWlhKek1Sc3dHUVlEVlFRREV4SmtiMk5yWlhJdFptOXlMV1JsCmMydDBiM0F3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRGxBTWZKVUUvWUIwb0UKWmN5TmE1S0dVMkZpRmNYLys2dFJQUlpETkhZSkE1ZGtaME40UC9kVVdZeWJGRlVYc0E3UU1Sbm1mS280Q25MTQptK28wS2NUd3NRMnY3UzlPejVJYlJCOVZGVnFqNDJmNW9mVFFDcnZNN20wWVovNlRzcjhtSDE0QVYzWkRZaWtsCkF1VjlqRUgvczF5WWppbG0rODVlbm02RUZYYkJMV2czcXZkQ3VxNmlMa2FjWFptWTJYVXVtTWVOTVZnQllrS1UKVi84czJ0VlhyTlhvaU9qZVFMZlIvYmpvYkFZbzlMM0JWZFczYUxjanBwcDYzWmE0YlZITHYyQ2ZXMDcwNjNvbQpYZ0syM1hHWjQwdFFGaElxbUlvZktYZ0lVSWk2YVV3UVI0WFVRd3RMeTk4aHRDazZ2ckl5UUpMWkdKV29WVlU4CitJclVtZFRyQWdNQkFBR2pKekFsTUE0R0ExVWREd0VCL3dRRUF3SUZvREFUQmdOVkhTVUVEREFLQmdnckJnRUYKQlFjREFqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFRRzlVaFVjUDdJQzdyZmRyK1pxTTBKbGxITzY3MzZoTgpVRkpvNmZSRUdDbjlxclN6SW44K0RZV1N3RnF4ZVRhZlNFK3VJZHFGREQ1ais1bWhEQzBzZUV2WWlNQ09CZFJDCk4xT3RRK1lrQndndnNKU3RxZGdzNTRXdkJwLzFiS09leFNLS1laTzJPaUJLd3NRV1ZXeksrQ0VjOXhRSm1jN2MKZGhlK0tNZVNTeC9LSmR0bHc4VWVSUkVCOU00WjZjRHpLYzQ2cjhBd04zWkxibzdsYzVCNE0wb2lXMUVwR0wwQgpKUXYrT1FDblV4K0d1dVcvTGdNT1JQRVFXaXF1UjFvWXlJQjlRb0wxRXFCTDZHejhTWjVtbTE1ellWSVZXTHh6ClNQLzVyd2VjNDY0Z216RDR4MHJVUHBIaWlRSVJzUjk1WXBIMjNxWkl2QlVwd2dnTjJnd2hTUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K\n    client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBNVFESHlWQlAyQWRLQkdYTWpXdVNobE5oWWhYRi8vdXJVVDBXUXpSMkNRT1haR2RECmVELzNWRm1NbXhSVkY3QU8wREVaNW55cU9BcHl6SnZxTkNuRThMRU5yKzB2VHMrU0cwUWZWUlZhbytObithSDAKMEFxN3pPNXRHR2YrazdLL0poOWVBRmQyUTJJcEpRTGxmWXhCLzdOY21JNHBadnZPWHA1dWhCVjJ3UzFvTjZyMwpRcnF1b2k1R25GMlptTmwxTHBqSGpURllBV0pDbEZmL0xOclZWNnpWNklqbzNrQzMwZjI0Nkd3R0tQUzl3VlhWCnQyaTNJNmFhZXQyV3VHMVJ5NzlnbjF0TzlPdDZKbDRDdHQxeG1lTkxVQllTS3BpS0h5bDRDRkNJdW1sTUVFZUYKMUVNTFM4dmZJYlFwT3I2eU1rQ1MyUmlWcUZWVlBQaUsxSm5VNndJREFRQUJBb0lCQUZSY29EenlZQ2VXTDlkRQo1VUVuNHRlbk9kWFhiWlNxMHViZm1TYnkyWlRpaE5BUkZwTGpCYXRHUGYwWFZXMmZoeVY5SVN4K3VucGdwdi9uClpEVUpPaXJ0SHJ5enBOemtyTTlzbmhwSy9wUW5mek5BVFo2aWhhS3VKdlI1d3hnSUhsRGQ5MVFxNUQ5WWx3MnkKYm5aOHlBZDV2Ti9hWnpnd0JVdG9GQkNHazdQLzRxK0JlbHZoNWd1SzdzS0dvRi94dGMwWlp6RGtYMkw5VHJlZQowSE5nTmJlYm91SHhlVHBkcGNLQzZ2TENST2tqb0RTdDY1ZEo2ajBGTzhzTERVcWRrWkxNa0ducTdoZWhYV2JwClBtRVI5dWc3Qk1HVUFtcHhpbGdGVHM0MnRSNnoxZXdvSWs5bC92V3ZMTFpZbEE3OUo4YkU0UTNPZGpXc2Nza1YKV0ZpakZ0a0NnWUVBNXU2SnV3b3A0Z2QzTjNhUHVhakpHNEpjYTZNWFZQNEVVay9vY2pobloyRDF5cjd2ZXhZSApVaE1WN2p6dzJUQ1FJa2JtMWZlSTZpa1llUzNVNXprZDhKSm9VaXV4T3ZsS0VJSlFrTEROQyttMHFuN0xEamU1Cmx0SkE3Sm9IdDhSTzNMS1JBTFhvQTVsV0l0NWNQLzVuTW1IMTlDZGQ3L294MU0xRXFFYUxNMmNDZ1lFQS9keWsKMExyR0VtbTg0SlU0dDIvZDVzNm5ERnAxOWxhUXJlbFY1bWRsZEFKNGRPVHkxZXV4dHNFeS8xSFExWXBLa25aSgpTa2Q2RTJzYk1MUncxdzFGWTZpczI0ektmaWtLV0N5SXBPMTkvQWh5UkpweGxKTlN4a2hjK1FpVXVSd1lsVmtMCi9XQ3dFUFVVaDI1VHJRNE9LRTNKazh3VmVmdFlwdkZNRDhvaHc5MENnWUF5ZTkxQ05XT1lsUmM3MmNCcnp2ay8KK1V5by96dGZpalI1cGh4anMrN3ZDNlJRRVZPYkxlS2x6NlJRczZQWFp5VnJTT0szemVoeGdGQm9WVnVndkx6TgoxY1BXaXRTdzFzU1pQVlBOZmNrbG5JNnhZd3lTN0IyM1dmbDFmK3JHQXJWV3kvYWxHQjlEZ2lieGNuanFTSHhZCjZFOXpjNU8yblpSOU4rNlZkdTZCYXdLQmdDV3Vtc2hnOFFYS3JENnA1OEZTMloxcEQyTEdDcnlHSFBPenJ3eUUKVElycjB2V0hCb1M2ZDZhcEJ1amZQQ0IyWnB0Vzg0b1RFZ3ZQMmpsZ2oxOWNtUEF5R1haOWI1RktoajZRWGJnZAppSlhncXhXRDExZzJoaExvcXVSTVljY1laSTNHcWdEeVdUQXJNT0RwZjRJd2srbG5vb1JOeHVKVWJOUmEvTzliCkVhZ0JBb0dBUE9VZk15M3JPSWRBRTV4Q0VxOUlza1hXblFrcmRwYktKVDVzbVFyVUhuRFh4QWM5L0libm1jWmwKOWN4Y3czdktMbWVZU3loWFF5ZWF1VXo3amZhdjZ3TGhnVi9KK1NYdlBlUng2aDFmb0lsVUF4WDJMdDFSOEduZApNejhqdHJxN29ycE1EQU9xOHNyaGxzZkZDYnJtUFZKSnNTd1J4R3ZJN3ZLTm54VXRXVFE9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg=="
  },
  {
    "path": "internal/config/testdata/kubes/test.yaml",
    "content": "apiVersion: v1\nkind: Config\nclusters:\n  - cluster:\n      certificate-authority: /Users/test/ca.crt\n      server: https://1.2.3.4:8443\n    name: cl-1\n  - cluster:\n      certificate-authority: /Users/test/ca.crt\n      server: https://5.6.7.8:8443\n    name: cl-2\ncontexts:\n  - context:\n      cluster: cl-1\n      user: user1\n      namespace: ns-1\n    name: ct-1-1\n  - context:\n      cluster: cl-1\n      user: user2\n      namespace: ns-2\n    name: ct-1-2\n  - context:\n      cluster: cl-2\n      user: user2\n      namespace: ns-2\n    name: ct-2-1\ncurrent-context: ct-1-1\npreferences: {}\nusers:\n  - name: user1\n    user:\n      client-certificate: /Users/test/client.crt\n      client-key: /Users/test/client.key\n  - name: user2\n    user:\n      client-certificate: /Users/test/client.crt\n      client-key: /Users/test/client.key\n"
  },
  {
    "path": "internal/config/testdata/plugins/dir/snippet.1.yaml",
    "content": "shortCut: shift-s\ndescription: blee\nscopes:\n  - po\n  - dp\ncommand: duh\nargs:\n  - -n\n  - $NAMESPACE\n  - -boolean\nbackground: false\nconfirm: true\noverwriteOutput: true\n"
  },
  {
    "path": "internal/config/testdata/plugins/dir/snippet.2.yaml",
    "content": "shortCut: shift-r\nconfirm: false\ndescription: bla\nscopes:\n  - svc\n  - ing\ncommand: duha\nbackground: true\nargs:\n  - -n\n  - $NAMESPACE\n  - -oyaml\n"
  },
  {
    "path": "internal/config/testdata/plugins/dir/snippet.multi.yaml",
    "content": "crapola:\n  shortCut: Shift-1\n  description: crapola\n  scopes:\n    - pods\n  command: crapola\n  background: false\n\nbozo:\n  shortCut: Shift-2\n  description: bozo\n  scopes:\n    - pods\n    - svc\n  command: bozo"
  },
  {
    "path": "internal/config/testdata/plugins/plugins-toast.yaml",
    "content": "plugins:\n  blah:\n    shortCut: shift-s\n    confirm: true\n    description: blee\n    scoped:\n      - po\n      - dp\n    command: duh\n    background: false\n    args:\n      - -n\n      - $NAMESPACE\n      - -boolean\n"
  },
  {
    "path": "internal/config/testdata/plugins/plugins.yaml",
    "content": "plugins:\n  blah:\n    shortCut: shift-s\n    confirm: true\n    description: blee\n    scopes:\n      - po\n      - dp\n    command: duh\n    background: false\n    args:\n      - -n\n      - $NAMESPACE\n      - -boolean\n"
  },
  {
    "path": "internal/config/testdata/skins/black-and-wtf.yaml",
    "content": "k9s:\n  body:\n    fgColor: white\n    bgColor: black\n    logoColor: white\n  info:\n    fgColor: navajowhite\n    sectionColor: white\n  frame:\n    border:\n      fgColor: white\n      focusColor: white\n    menu:\n      fgColor: white\n      keyColor: white\n      numKeyColor: navajowhite\n    crumb:\n      fgColor: black\n      bgColor: navajowhite\n      activeColor: whitesmoke\n    status:\n      newColor: ghostwhite\n      modifyColor: navajowhite\n      addColor: darkslategray\n      errorColor: whitesmoke\n      highlightcolor: dimgray\n      killColor: slategray\n      completedColor: gray\n    title:\n      fgColor: ghostwhite\n      highlightColor: navajowhite\n      counterColor: navajowhite\n      filterColor: slategray\n  views:\n    table:\n      fgColor: white\n      bgColor: black\n      cursorColor: white\n      header:\n        fgColor: darkgray\n        bgColor: black\n        sorterColor: white\n"
  },
  {
    "path": "internal/config/testdata/skins/boarked.yaml",
    "content": "k9s:\n  fgColor: blee\n  bgColor: black\n  logoColor: white\n  info:\n    - fgColor: fred\n    - sectionColor: white\n"
  },
  {
    "path": "internal/config/testdata/skins/empty.yaml",
    "content": "k9s:\n  body:"
  },
  {
    "path": "internal/config/testdata/views/views.yaml",
    "content": "views:\n  v1/pods:\n    columns:\n      - NAMESPACE\n      - NAME\n      - AGE\n      - IP\n\n  v1/pods@default:\n    columns:\n      - NAME\n      - IP\n      - AGE\n\n  v1/pods@ns*:\n    columns:\n      - AGE\n      - NAME\n      - IP\n\n  bozo:\n    columns:\n      - DUH\n      - BLAH\n      - BLEE\n"
  },
  {
    "path": "internal/config/threshold.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nconst (\n\t// SeverityLow tracks low severity.\n\tSeverityLow SeverityLevel = iota\n\n\t// SeverityMedium tracks medium severity level.\n\tSeverityMedium\n\n\t// SeverityHigh tracks high severity level.\n\tSeverityHigh\n)\n\n// SeverityLevel tracks severity levels.\ntype SeverityLevel int\n\n// Severity tracks a resource severity levels.\ntype Severity struct {\n\tCritical int `yaml:\"critical\"`\n\tWarn     int `yaml:\"warn\"`\n}\n\n// NewSeverity returns a new instance.\nfunc NewSeverity() *Severity {\n\treturn &Severity{\n\t\tCritical: 90,\n\t\tWarn:     70,\n\t}\n}\n\n// Validate checks all thresholds and make sure we're cool. If not use defaults.\nfunc (s *Severity) Validate() {\n\tnorm := NewSeverity()\n\tif !validateRange(s.Warn) {\n\t\ts.Warn = norm.Warn\n\t}\n\tif !validateRange(s.Critical) {\n\t\ts.Critical = norm.Critical\n\t}\n}\n\nfunc validateRange(v int) bool {\n\tif v <= 0 || v > 100 {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// Threshold tracks threshold to alert user when exceeded.\ntype Threshold map[string]*Severity\n\n// NewThreshold returns a new threshold.\nfunc NewThreshold() Threshold {\n\treturn Threshold{\n\t\tCPU: NewSeverity(),\n\t\tMEM: NewSeverity(),\n\t}\n}\n\n// Validate a namespace is setup correctly.\nfunc (t Threshold) Validate() Threshold {\n\tfor _, k := range []string{CPU, MEM} {\n\t\tv, ok := t[k]\n\t\tif !ok {\n\t\t\tt[k] = NewSeverity()\n\t\t} else {\n\t\t\tv.Validate()\n\t\t}\n\t}\n\n\treturn t\n}\n\n// LevelFor returns a defcon level for the current state.\nfunc (t Threshold) LevelFor(k string, v int) SeverityLevel {\n\ts, ok := t[k]\n\tif !ok || v < 0 || v > 100 {\n\t\treturn SeverityLow\n\t}\n\tif v >= s.Critical {\n\t\treturn SeverityHigh\n\t}\n\tif v >= s.Warn {\n\t\treturn SeverityMedium\n\t}\n\n\treturn SeverityLow\n}\n\n// SeverityColor returns a defcon level associated level.\nfunc (t *Threshold) SeverityColor(k string, v int) string {\n\t//nolint:exhaustive\n\tswitch t.LevelFor(k, v) {\n\tcase SeverityHigh:\n\t\treturn \"red\"\n\tcase SeverityMedium:\n\t\treturn \"orangered\"\n\tdefault:\n\t\treturn \"green\"\n\t}\n}\n"
  },
  {
    "path": "internal/config/threshold_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSeverityValidate(t *testing.T) {\n\tuu := map[string]struct {\n\t\td, e *config.Severity\n\t}{\n\t\t\"default\": {\n\t\t\td: config.NewSeverity(),\n\t\t\te: config.NewSeverity(),\n\t\t},\n\t\t\"toast\": {\n\t\t\td: &config.Severity{Warn: 10},\n\t\t\te: &config.Severity{Warn: 10, Critical: 90},\n\t\t},\n\t\t\"negative\": {\n\t\t\td: &config.Severity{Warn: -1},\n\t\t\te: config.NewSeverity(),\n\t\t},\n\t\t\"out-of-range\": {\n\t\t\td: &config.Severity{Warn: 150},\n\t\t\te: config.NewSeverity(),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tu.d.Validate()\n\t\t\tassert.Equal(t, u.e, u.d)\n\t\t})\n\t}\n}\n\nfunc TestLevelFor(t *testing.T) {\n\tuu := map[string]struct {\n\t\tk string\n\t\tv int\n\t\te config.SeverityLevel\n\t}{\n\t\t\"normal\": {\n\t\t\tk: config.CPU,\n\t\t\tv: 0,\n\t\t\te: config.SeverityLow,\n\t\t},\n\t\t\"4\": {\n\t\t\tk: config.CPU,\n\t\t\tv: 71,\n\t\t\te: config.SeverityMedium,\n\t\t},\n\t\t\"3\": {\n\t\t\tk: config.CPU,\n\t\t\tv: 75,\n\t\t\te: config.SeverityMedium,\n\t\t},\n\t\t\"2\": {\n\t\t\tk: config.CPU,\n\t\t\tv: 80,\n\t\t\te: config.SeverityMedium,\n\t\t},\n\t\t\"1\": {\n\t\t\tk: config.CPU,\n\t\t\tv: 100,\n\t\t\te: config.SeverityHigh,\n\t\t},\n\t\t\"over\": {\n\t\t\tk: config.CPU,\n\t\t\tv: 150,\n\t\t\te: config.SeverityLow,\n\t\t},\n\t\t\"over-mem\": {\n\t\t\tk: config.MEM,\n\t\t\tv: 150,\n\t\t\te: config.SeverityLow,\n\t\t},\n\t}\n\n\to := config.NewThreshold()\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, o.LevelFor(u.k, u.v))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/config/types.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nconst (\n\tdefaultRefreshRate  = 2\n\tdefaultMaxConnRetry = 5\n\n\t// CPU tracks cpu usage.\n\tCPU = \"cpu\"\n\n\t// MEM tracks memory usage.\n\tMEM = \"memory\"\n)\n\n// UI tracks ui specific configs.\ntype UI struct {\n\t// EnableMouse toggles mouse support.\n\tEnableMouse bool `json:\"enableMouse\" yaml:\"enableMouse\"`\n\n\t// Headless toggles top header display.\n\tHeadless bool `json:\"headless\" yaml:\"headless\"`\n\n\t// LogoLess toggles k9s logo.\n\tLogoless bool `json:\"logoless\" yaml:\"logoless\"`\n\n\t// Crumbsless toggles nav crumb display.\n\tCrumbsless bool `json:\"crumbsless\" yaml:\"crumbsless\"`\n\n\t// Splashless disables the splash screen on startup.\n\tSplashless bool `json:\"splashless\" yaml:\"splashless\"`\n\n\t// Reactive toggles reactive ui changes.\n\tReactive bool `json:\"reactive\" yaml:\"reactive\"`\n\n\t// NoIcons toggles icons display.\n\tNoIcons bool `json:\"noIcons\" yaml:\"noIcons\"`\n\n\t// Invert inverts all skin colors using Oklch lightness inversion.\n\tInvert bool `json:\"invert\" yaml:\"invert\"`\n\n\t// Skin reference the general k9s skin name.\n\t// Can be overridden per context.\n\tSkin string `json:\"skin\" yaml:\"skin,omitempty\"`\n\n\t// DefaultsToFullScreen toggles fullscreen on views like logs, yaml, details.\n\tDefaultsToFullScreen bool `json:\"defaultsToFullScreen\" yaml:\"defaultsToFullScreen\"`\n\n\t// UseFullGVRTitle toggles the display of full GVR (group/version/resource) vs R in views title.\n\tUseFullGVRTitle bool `json:\"useFullGVRTitle\" yaml:\"useFullGVRTitle\"`\n\n\tmanualHeadless   *bool\n\tmanualLogoless   *bool\n\tmanualCrumbsless *bool\n\tmanualSplashless *bool\n\tmanualInvert     *bool\n}\n"
  },
  {
    "path": "internal/config/views.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nimport (\n\t\"cmp\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"maps\"\n\t\"os\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/config/json\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"gopkg.in/yaml.v3\"\n)\n\n// ViewConfigListener represents a view config listener.\ntype ViewConfigListener interface {\n\t// ViewSettingsChanged notifies listener the view configuration changed.\n\tViewSettingsChanged(*ViewSetting)\n\n\t// GetNamespace return the view namespace\n\tGetNamespace() string\n}\n\n// ViewSetting represents a view configuration.\ntype ViewSetting struct {\n\tColumns    []string `yaml:\"columns\"`\n\tSortColumn string   `yaml:\"sortColumn\"`\n}\n\nfunc (v *ViewSetting) HasCols() bool {\n\treturn len(v.Columns) > 0\n}\n\nfunc (v *ViewSetting) IsBlank() bool {\n\treturn v == nil || (len(v.Columns) == 0 && v.SortColumn == \"\")\n}\n\nfunc (v *ViewSetting) SortCol() (name string, asc bool, err error) {\n\tif v == nil || v.SortColumn == \"\" {\n\t\treturn \"\", false, fmt.Errorf(\"no sort column specified\")\n\t}\n\ttt := strings.Split(v.SortColumn, \":\")\n\tif len(tt) < 2 {\n\t\treturn \"\", false, fmt.Errorf(\"invalid sort column spec: %q. must be col-name:asc|desc\", v.SortColumn)\n\t}\n\n\treturn tt[0], tt[1] == \"asc\", nil\n}\n\n// Equals checks if two view settings are equal.\nfunc (v *ViewSetting) Equals(vs *ViewSetting) bool {\n\tif v == nil && vs == nil {\n\t\treturn true\n\t}\n\tif v == nil || vs == nil {\n\t\treturn false\n\t}\n\n\tif c := slices.Compare(v.Columns, vs.Columns); c != 0 {\n\t\treturn false\n\t}\n\n\treturn cmp.Compare(v.SortColumn, vs.SortColumn) == 0\n}\n\n// CustomView represents a collection of view customization.\ntype CustomView struct {\n\tViews     map[string]ViewSetting `yaml:\"views\"`\n\tlisteners map[string]ViewConfigListener\n}\n\n// NewCustomView returns a views configuration.\nfunc NewCustomView() *CustomView {\n\treturn &CustomView{\n\t\tViews:     make(map[string]ViewSetting),\n\t\tlisteners: make(map[string]ViewConfigListener),\n\t}\n}\n\n// Reset clears out configurations.\nfunc (v *CustomView) Reset() {\n\tfor k := range v.Views {\n\t\tdelete(v.Views, k)\n\t}\n}\n\n// Load loads view configurations.\nfunc (v *CustomView) Load(path string) error {\n\tif _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) {\n\t\treturn nil\n\t}\n\tbb, err := os.ReadFile(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif err := data.JSONValidator.Validate(json.ViewsSchema, bb); err != nil {\n\t\tslog.Warn(\"Validation failed. Please update your config and restart!\",\n\t\t\tslogs.Path, path,\n\t\t\tslogs.Error, err,\n\t\t)\n\t}\n\tvar in CustomView\n\tif err := yaml.Unmarshal(bb, &in); err != nil {\n\t\treturn err\n\t}\n\tv.Views = in.Views\n\tv.fireConfigChanged()\n\n\treturn nil\n}\n\n// AddListeners registers a new listener for various commands.\nfunc (v *CustomView) AddListeners(l ViewConfigListener, cmds ...string) {\n\tfor _, cmd := range cmds {\n\t\tif cmd != \"\" {\n\t\t\tv.listeners[cmd] = l\n\t\t}\n\t}\n\tv.fireConfigChanged()\n}\n\n// AddListener registers a new listener.\nfunc (v *CustomView) AddListener(cmd string, l ViewConfigListener) {\n\tv.listeners[cmd] = l\n\tv.fireConfigChanged()\n}\n\n// RemoveListener unregister a listener.\nfunc (v *CustomView) RemoveListener(l ViewConfigListener) {\n\tfor k, list := range v.listeners {\n\t\tif list == l {\n\t\t\tdelete(v.listeners, k)\n\t\t}\n\t}\n}\n\nfunc (v *CustomView) fireConfigChanged() {\n\tcmds := slices.Collect(maps.Keys(v.listeners))\n\tslices.SortFunc(cmds, func(a, b string) int {\n\t\tswitch {\n\t\tcase strings.Contains(a, \"/\") && !strings.Contains(b, \"/\"):\n\t\t\treturn 1\n\t\tcase !strings.Contains(a, \"/\") && strings.Contains(b, \"/\"):\n\t\t\treturn -1\n\t\tdefault:\n\t\t\treturn strings.Compare(a, b)\n\t\t}\n\t})\n\ttype tuple struct {\n\t\tcmd string\n\t\tvs  *ViewSetting\n\t}\n\tvar victim tuple\n\tfor _, cmd := range cmds {\n\t\tif vs := v.getVS(cmd, v.listeners[cmd].GetNamespace()); vs != nil {\n\t\t\tslog.Debug(\"Reloading custom view settings\", slogs.Command, cmd)\n\t\t\tvictim = tuple{cmd, vs}\n\t\t\tbreak\n\t\t}\n\t\tvictim = tuple{cmd, nil}\n\t}\n\tif victim.cmd != \"\" {\n\t\tv.listeners[victim.cmd].ViewSettingsChanged(victim.vs)\n\t}\n}\n\nfunc (v *CustomView) getVS(gvr, ns string) *ViewSetting {\n\tif client.IsAllNamespaces(ns) {\n\t\tns = client.NamespaceAll\n\t}\n\tk := gvr\n\tkk := slices.Collect(maps.Keys(v.Views))\n\tslices.SortFunc(kk, strings.Compare)\n\tslices.Reverse(kk)\n\tfor _, key := range kk {\n\t\tif !strings.HasPrefix(key, gvr) && !strings.HasPrefix(gvr, key) {\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch {\n\t\tcase strings.Contains(key, \"@\"):\n\t\t\ttt := strings.Split(key, \"@\")\n\t\t\tif len(tt) != 2 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tnsk := gvr\n\t\t\tif ns != \"\" {\n\t\t\t\tnsk += \"@\" + ns\n\t\t\t}\n\t\t\tif rx, err := regexp.Compile(tt[1]); err == nil && rx.MatchString(nsk) {\n\t\t\t\tvs := v.Views[key]\n\t\t\t\treturn &vs\n\t\t\t}\n\t\tcase strings.HasPrefix(k, key):\n\t\t\tkk := strings.Fields(k)\n\t\t\tif len(kk) == 2 {\n\t\t\t\tif v, ok := v.Views[kk[0]+\"@\"+kk[1]]; ok {\n\t\t\t\t\treturn &v\n\t\t\t\t}\n\t\t\t\tif key == kk[0] {\n\t\t\t\t\tvs := v.Views[key]\n\t\t\t\t\treturn &vs\n\t\t\t\t}\n\t\t\t}\n\t\t\tfallthrough\n\t\tcase key == k:\n\t\t\tvs := v.Views[key]\n\t\t\treturn &vs\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/config/views_int_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCustomView_getVS(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcv      *CustomView\n\t\tgvr, ns string\n\t\te       *ViewSetting\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"miss\": {\n\t\t\tgvr: \"zorg\",\n\t\t},\n\n\t\t\"gvr\": {\n\t\t\tgvr: client.PodGVR.String(),\n\t\t\te: &ViewSetting{\n\t\t\t\tColumns: []string{\"NAMESPACE\", \"NAME\", \"AGE\", \"IP\"},\n\t\t\t},\n\t\t},\n\n\t\t\"gvr+ns\": {\n\t\t\tgvr: client.PodGVR.String(),\n\t\t\tns:  \"default\",\n\t\t\te: &ViewSetting{\n\t\t\t\tColumns: []string{\"NAME\", \"IP\", \"AGE\"},\n\t\t\t},\n\t\t},\n\n\t\t\"rx\": {\n\t\t\tgvr: client.PodGVR.String(),\n\t\t\tns:  \"ns-fred\",\n\t\t\te: &ViewSetting{\n\t\t\t\tColumns: []string{\"AGE\", \"NAME\", \"IP\"},\n\t\t\t},\n\t\t},\n\n\t\t\"alias\": {\n\t\t\tgvr: \"bozo\",\n\t\t\te: &ViewSetting{\n\t\t\t\tColumns: []string{\"DUH\", \"BLAH\", \"BLEE\"},\n\t\t\t},\n\t\t},\n\n\t\t\"toast-no-ns\": {\n\t\t\tgvr: client.PodGVR.String(),\n\t\t\tns:  \"zorg\",\n\t\t\te: &ViewSetting{\n\t\t\t\tColumns: []string{\"NAMESPACE\", \"NAME\", \"AGE\", \"IP\"},\n\t\t\t},\n\t\t},\n\n\t\t\"toast-no-res\": {\n\t\t\tgvr: client.SvcGVR.String(),\n\t\t\tns:  \"zorg\",\n\t\t},\n\t}\n\n\tv := NewCustomView()\n\trequire.NoError(t, v.Load(\"testdata/views/views.yaml\"))\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, v.getVS(u.gvr, u.ns))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/config/views_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage config_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc init() {\n\tslog.SetDefault(slog.New(slog.DiscardHandler))\n}\n\nfunc TestCustomViewLoad(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcv   *config.CustomView\n\t\tpath string\n\t\tkey  string\n\t\te    []string\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"gvr\": {\n\t\t\tpath: \"testdata/views/views.yaml\",\n\t\t\tkey:  client.PodGVR.String(),\n\t\t\te:    []string{\"NAMESPACE\", \"NAME\", \"AGE\", \"IP\"},\n\t\t},\n\n\t\t\"gvr+ns\": {\n\t\t\tpath: \"testdata/views/views.yaml\",\n\t\t\tkey:  \"v1/pods@default\",\n\t\t\te:    []string{\"NAME\", \"IP\", \"AGE\"},\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tcfg := config.NewCustomView()\n\n\t\t\trequire.NoError(t, cfg.Load(u.path))\n\t\t\tassert.Equal(t, u.e, cfg.Views[u.key].Columns)\n\t\t})\n\t}\n}\n\nfunc TestViewSettingEquals(t *testing.T) {\n\tuu := map[string]struct {\n\t\tv1, v2 *config.ViewSetting\n\t\te      bool\n\t}{\n\t\t\"v1-nil-v2-nil\": {\n\t\t\te: true,\n\t\t},\n\n\t\t\"v1-v2-empty\": {\n\t\t\tv1: new(config.ViewSetting),\n\t\t\tv2: new(config.ViewSetting),\n\t\t\te:  true,\n\t\t},\n\n\t\t\"v1-nil\": {\n\t\t\tv1: new(config.ViewSetting),\n\t\t},\n\n\t\t\"nil-v2\": {\n\t\t\tv2: new(config.ViewSetting),\n\t\t},\n\n\t\t\"v1-v2-blank\": {\n\t\t\tv1: &config.ViewSetting{\n\t\t\t\tColumns: []string{\"A\"},\n\t\t\t},\n\t\t\tv2: new(config.ViewSetting),\n\t\t},\n\n\t\t\"v1-v2-nil\": {\n\t\t\tv1: &config.ViewSetting{\n\t\t\t\tColumns: []string{\"A\"},\n\t\t\t},\n\t\t},\n\n\t\t\"same\": {\n\t\t\tv1: &config.ViewSetting{\n\t\t\t\tColumns: []string{\"A\", \"B\", \"C\"},\n\t\t\t},\n\t\t\tv2: &config.ViewSetting{\n\t\t\t\tColumns: []string{\"A\", \"B\", \"C\"},\n\t\t\t},\n\t\t\te: true,\n\t\t},\n\n\t\t\"order\": {\n\t\t\tv1: &config.ViewSetting{\n\t\t\t\tColumns: []string{\"C\", \"A\", \"B\"},\n\t\t\t},\n\t\t\tv2: &config.ViewSetting{\n\t\t\t\tColumns: []string{\"A\", \"B\", \"C\"},\n\t\t\t},\n\t\t},\n\n\t\t\"delta\": {\n\t\t\tv1: &config.ViewSetting{\n\t\t\t\tColumns: []string{\"A\", \"B\", \"C\"},\n\t\t\t},\n\t\t\tv2: &config.ViewSetting{\n\t\t\t\tColumns: []string{\"B\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equalf(t, u.e, u.v1.Equals(u.v2), \"%#v and %#v\", u.v1, u.v2)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dao/accessor.go",
    "content": "package dao\n\nimport (\n\t\"log/slog\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n)\n\nvar accessors = Accessors{\n\tclient.WkGVR:  new(Workload),\n\tclient.CtGVR:  new(Context),\n\tclient.CoGVR:  new(Container),\n\tclient.ScnGVR: new(ImageScan),\n\tclient.SdGVR:  new(ScreenDump),\n\tclient.BeGVR:  new(Benchmark),\n\tclient.PfGVR:  new(PortForward),\n\tclient.DirGVR: new(Dir),\n\n\tclient.SvcGVR:  new(Service),\n\tclient.PodGVR:  new(Pod),\n\tclient.NodeGVR: new(Node),\n\tclient.NsGVR:   new(Namespace),\n\tclient.CmGVR:   new(ConfigMap),\n\tclient.SecGVR:  new(Secret),\n\n\tclient.DpGVR:  new(Deployment),\n\tclient.DsGVR:  new(DaemonSet),\n\tclient.StsGVR: new(StatefulSet),\n\tclient.RsGVR:  new(ReplicaSet),\n\n\tclient.CjGVR:  new(CronJob),\n\tclient.JobGVR: new(Job),\n\n\tclient.HmGVR:  new(HelmChart),\n\tclient.HmhGVR: new(HelmHistory),\n\n\tclient.CrdGVR: new(CustomResourceDefinition),\n}\n\n// Accessors represents a collection of dao accessors.\ntype Accessors map[*client.GVR]Accessor\n\n// AccessorFor returns a client accessor for a resource if registered.\n// Otherwise it returns a generic accessor.\n// Customize here for non resource types or types with metrics or logs.\nfunc AccessorFor(f Factory, gvr *client.GVR) (Accessor, error) {\n\tr, ok := accessors[gvr]\n\tif !ok {\n\t\tr = new(Scaler)\n\t\tslog.Debug(\"No DAO registry entry. Using generics!\", slogs.GVR, gvr)\n\t}\n\tr.Init(f, gvr)\n\n\treturn r, nil\n}\n"
  },
  {
    "path": "internal/dao/alias.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/util/sets\"\n)\n\nvar _ Accessor = (*Alias)(nil)\n\n// Alias tracks standard and custom command aliases.\ntype Alias struct {\n\tNonResource\n\n\t*config.Aliases\n}\n\n// NewAlias returns a new set of aliases.\nfunc NewAlias(f Factory) *Alias {\n\ta := Alias{\n\t\tAliases: config.NewAliases(),\n\t}\n\ta.Init(f, client.AliGVR)\n\n\treturn &a\n}\n\n// AliasesFor returns a set of aliases for a given gvr.\nfunc (a *Alias) AliasesFor(gvr *client.GVR) sets.Set[string] {\n\treturn a.Aliases.AliasesFor(gvr)\n}\n\n// List returns a collection of aliases.\nfunc (*Alias) List(ctx context.Context, _ string) ([]runtime.Object, error) {\n\taa, ok := ctx.Value(internal.KeyAliases).(*Alias)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"expecting *Alias but got %T\", ctx.Value(internal.KeyAliases))\n\t}\n\tm := aa.ShortNames()\n\too := make([]runtime.Object, 0, len(m))\n\tfor gvr, aliases := range m {\n\t\tsort.StringSlice(aliases).Sort()\n\t\too = append(oo, render.AliasRes{\n\t\t\tGVR:     gvr,\n\t\t\tAliases: aliases,\n\t\t})\n\t}\n\n\treturn oo, nil\n}\n\n// Get fetch a resource.\nfunc (*Alias) Get(_ context.Context, _ string) (runtime.Object, error) {\n\treturn nil, errors.New(\"nyi\")\n}\n\n// Ensure makes sure alias are loaded.\nfunc (a *Alias) Ensure(path string) (config.Alias, error) {\n\tif err := MetaAccess.LoadResources(a.Factory); err != nil {\n\t\treturn config.Alias{}, err\n\t}\n\treturn a.Alias, a.load(path)\n}\n\nfunc (a *Alias) load(path string) error {\n\tif err := a.Load(path); err != nil {\n\t\treturn err\n\t}\n\n\tcrdGVRS := make(client.GVRs, 0, 50)\n\tfor _, gvr := range MetaAccess.AllGVRs() {\n\t\tmeta, err := MetaAccess.MetaFor(gvr)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif IsK9sMeta(meta) {\n\t\t\tcontinue\n\t\t}\n\t\tif IsCRD(meta) {\n\t\t\tcrdGVRS = append(crdGVRS, gvr)\n\t\t\tcontinue\n\t\t}\n\t\ta.Define(gvr, gvr.AsResourceName())\n\n\t\t// Allow single shot commands for k8s resources only expect for metrics resource which override pods and nodes ;(!\n\t\tif isStandardGroup(gvr.GVSub()) && gvr.G() != \"metrics.k8s.io\" {\n\t\t\ta.Define(gvr, meta.Name, meta.SingularName)\n\t\t}\n\t\tif len(meta.ShortNames) > 0 {\n\t\t\ta.Define(gvr, meta.ShortNames...)\n\t\t}\n\t\ta.Define(gvr, gvr.String())\n\t}\n\n\tfor _, gvr := range crdGVRS {\n\t\tmeta, err := MetaAccess.MetaFor(gvr)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ta.Define(gvr, strings.ToLower(meta.Kind), meta.Name)\n\t\ta.Define(gvr, meta.SingularName)\n\n\t\tif len(meta.ShortNames) > 0 {\n\t\t\ta.Define(gvr, meta.ShortNames...)\n\t\t}\n\t\ta.Define(gvr, gvr.String())\n\t\ta.Define(gvr, meta.Name+\".\"+meta.Group)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/dao/alias_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestAliasList(t *testing.T) {\n\ta := dao.Alias{}\n\ta.Init(makeFactory(), client.AliGVR)\n\n\tctx := context.WithValue(context.Background(), internal.KeyAliases, makeAliases())\n\too, err := a.List(ctx, \"-\")\n\n\trequire.NoError(t, err)\n\tassert.Len(t, oo, 2)\n\tassert.Len(t, oo[0].(render.AliasRes).Aliases, 2)\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc makeAliases() *dao.Alias {\n\tgvr1 := client.NewGVR(\"v1/fred\")\n\tgvr2 := client.NewGVR(\"v1/blee\")\n\n\treturn &dao.Alias{\n\t\tAliases: &config.Aliases{\n\t\t\tAlias: config.Alias{\n\t\t\t\t\"fred\": gvr1,\n\t\t\t\t\"f\":    gvr1,\n\t\t\t\t\"blee\": gvr2,\n\t\t\t\t\"b\":    gvr2,\n\t\t\t},\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/dao/benchmark.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/render\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar (\n\t_ Accessor = (*Benchmark)(nil)\n\t_ Nuker    = (*Benchmark)(nil)\n\n\tBenchRx = regexp.MustCompile(`[:|]+`)\n)\n\n// Benchmark represents a benchmark resource.\ntype Benchmark struct {\n\tNonResource\n}\n\n// Delete nukes a resource.\nfunc (*Benchmark) Delete(_ context.Context, path string, _ *metav1.DeletionPropagation, _ Grace) error {\n\treturn os.Remove(path)\n}\n\n// Get returns a resource.\nfunc (*Benchmark) Get(context.Context, string) (runtime.Object, error) {\n\tpanic(\"NYI\")\n}\n\n// List returns a collection of resources.\nfunc (*Benchmark) List(ctx context.Context, _ string) ([]runtime.Object, error) {\n\tdir, ok := ctx.Value(internal.KeyDir).(string)\n\tif !ok {\n\t\treturn nil, errors.New(\"no benchmark dir found in context\")\n\t}\n\tpath, ok := ctx.Value(internal.KeyPath).(string)\n\tif !ok {\n\t\treturn nil, errors.New(\"no path specified in context\")\n\t}\n\tpathMatch := BenchRx.ReplaceAllString(strings.Replace(path, \"/\", \"_\", 1), \"_\")\n\n\tff, err := os.ReadDir(dir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\too := make([]runtime.Object, 0, len(ff))\n\tfor _, f := range ff {\n\t\tif !strings.HasPrefix(f.Name(), pathMatch) {\n\t\t\tcontinue\n\t\t}\n\t\tif fi, err := f.Info(); err == nil {\n\t\t\too = append(oo, render.BenchInfo{File: fi, Path: filepath.Join(dir, f.Name())})\n\t\t}\n\t}\n\n\treturn oo, nil\n}\n"
  },
  {
    "path": "internal/dao/benchmark_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestBenchmarkList(t *testing.T) {\n\ta := dao.Benchmark{}\n\ta.Init(makeFactory(), client.BeGVR)\n\n\tctx := context.WithValue(context.Background(), internal.KeyDir, \"testdata/bench\")\n\tctx = context.WithValue(ctx, internal.KeyPath, \"\")\n\too, err := a.List(ctx, \"-\")\n\n\trequire.NoError(t, err)\n\tassert.Len(t, oo, 1)\n\tassert.Equal(t, \"testdata/bench/default_fred_1577308050814961000.txt\", oo[0].(render.BenchInfo).Path)\n}\n"
  },
  {
    "path": "internal/dao/cluster.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sync\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n)\n\n// RefScanner represents a resource reference scanner.\ntype RefScanner interface {\n\t// Init initializes the scanner\n\tInit(Factory, *client.GVR)\n\n\t// Scan scan the resource for references.\n\tScan(ctx context.Context, gvr *client.GVR, fqn string, wait bool) (Refs, error)\n\n\t// ScanSA scan the resource for serviceaccount references.\n\tScanSA(ctx context.Context, fqn string, wait bool) (Refs, error)\n}\n\n// Ref represents a resource reference.\ntype Ref struct {\n\tGVR string\n\tFQN string\n}\n\n// Refs represents a collection of resource references.\ntype Refs []Ref\n\nvar (\n\t_ RefScanner = (*Deployment)(nil)\n\t_ RefScanner = (*StatefulSet)(nil)\n\t_ RefScanner = (*DaemonSet)(nil)\n\t_ RefScanner = (*Job)(nil)\n\t_ RefScanner = (*CronJob)(nil)\n)\n\nfunc scanners() map[*client.GVR]RefScanner {\n\treturn map[*client.GVR]RefScanner{\n\t\tclient.DpGVR:  new(Deployment),\n\t\tclient.DsGVR:  new(DaemonSet),\n\t\tclient.StsGVR: new(StatefulSet),\n\t\tclient.CjGVR:  new(CronJob),\n\t\tclient.JobGVR: new(Job),\n\t}\n}\n\n// ScanForRefs scans cluster resources for resource references.\nfunc ScanForRefs(ctx context.Context, f Factory) (Refs, error) {\n\trgvr, ok := ctx.Value(internal.KeyGVR).(*client.GVR)\n\tif !ok {\n\t\treturn nil, errors.New(\"expecting context GVR\")\n\t}\n\tfqn, ok := ctx.Value(internal.KeyPath).(string)\n\tif !ok {\n\t\treturn nil, errors.New(\"expecting context Path\")\n\t}\n\twait, ok := ctx.Value(internal.KeyWait).(bool)\n\tif !ok {\n\t\tslog.Warn(\"Expecting context Wait key. Using default\")\n\t}\n\n\tvar wg sync.WaitGroup\n\tout := make(chan Refs)\n\tfor gvr, scanner := range scanners() {\n\t\twg.Add(1)\n\t\tgo func(ctx context.Context, gvr *client.GVR, s RefScanner, out chan Refs, wait bool) {\n\t\t\tdefer wg.Done()\n\t\t\ts.Init(f, gvr)\n\t\t\trefs, err := s.Scan(ctx, rgvr, fqn, wait)\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(\"Reference scan failed for\",\n\t\t\t\t\tslogs.RefType, fmt.Sprintf(\"%T\", s),\n\t\t\t\t\tslogs.Error, err,\n\t\t\t\t)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tselect {\n\t\t\tcase out <- refs:\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\t}\n\t\t}(ctx, gvr, scanner, out, wait)\n\t}\n\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(out)\n\t}()\n\n\tres := make(Refs, 0, 10)\n\tfor refs := range out {\n\t\tres = append(res, refs...)\n\t}\n\n\treturn res, nil\n}\n\n// ScanForSARefs scans cluster resources for serviceaccount refs.\nfunc ScanForSARefs(ctx context.Context, f Factory) (Refs, error) {\n\tfqn, ok := ctx.Value(internal.KeyPath).(string)\n\tif !ok {\n\t\treturn nil, errors.New(\"expecting context Path\")\n\t}\n\twait, ok := ctx.Value(internal.KeyWait).(bool)\n\tif !ok {\n\t\treturn nil, errors.New(\"expecting context Wait\")\n\t}\n\n\tvar wg sync.WaitGroup\n\tout := make(chan Refs)\n\tfor gvr, scanner := range scanners() {\n\t\twg.Add(1)\n\t\tgo func(ctx context.Context, gvr *client.GVR, s RefScanner, out chan Refs, wait bool) {\n\t\t\tdefer wg.Done()\n\t\t\ts.Init(f, gvr)\n\t\t\trefs, err := s.ScanSA(ctx, fqn, wait)\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(\"ServiceAccount scan failed\",\n\t\t\t\t\tslogs.RefType, fmt.Sprintf(\"%T\", s),\n\t\t\t\t\tslogs.Error, err,\n\t\t\t\t)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tselect {\n\t\t\tcase out <- refs:\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\t}\n\t\t}(ctx, gvr, scanner, out, wait)\n\t}\n\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(out)\n\t}()\n\n\tres := make(Refs, 0, 10)\n\tfor refs := range out {\n\t\tres = append(res, refs...)\n\t}\n\n\treturn res, nil\n}\n"
  },
  {
    "path": "internal/dao/cm.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nvar _ Accessor = (*ConfigMap)(nil)\n\n// ConfigMap represents a configmap resource.\ntype ConfigMap struct {\n\tResource\n}\n"
  },
  {
    "path": "internal/dao/container.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\tmv1beta1 \"k8s.io/metrics/pkg/apis/metrics/v1beta1\"\n)\n\nvar (\n\t_ Accessor = (*Container)(nil)\n\t_ Loggable = (*Container)(nil)\n)\n\nconst (\n\tinitIDX = \"I\"\n\tmainIDX = \"M\"\n\tephIDX  = \"E\"\n)\n\n// Container represents a pod's container dao.\ntype Container struct {\n\tNonResource\n}\n\n// List returns a collection of containers.\nfunc (c *Container) List(ctx context.Context, _ string) ([]runtime.Object, error) {\n\tfqn, ok := ctx.Value(internal.KeyPath).(string)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"no context path for %q\", c.gvr)\n\t}\n\n\tvar (\n\t\tcmx client.ContainersMetrics\n\t\terr error\n\t)\n\tif withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); ok && withMx {\n\t\tcmx, _ = client.DialMetrics(c.Client()).FetchContainersMetrics(ctx, fqn)\n\t}\n\n\tpo, err := c.fetchPod(fqn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tres := make([]runtime.Object, 0, len(po.Spec.InitContainers)+len(po.Spec.Containers)+len(po.Spec.EphemeralContainers))\n\tfor i := range po.Spec.InitContainers {\n\t\tres = append(res, makeContainerRes(\n\t\t\tinitIDX,\n\t\t\ti,\n\t\t\t&(po.Spec.InitContainers[i]),\n\t\t\tpo,\n\t\t\tcmx[po.Spec.InitContainers[i].Name]),\n\t\t)\n\t}\n\tfor i := range po.Spec.Containers {\n\t\tres = append(res, makeContainerRes(\n\t\t\tmainIDX,\n\t\t\ti,\n\t\t\t&(po.Spec.Containers[i]),\n\t\t\tpo,\n\t\t\tcmx[po.Spec.Containers[i].Name]),\n\t\t)\n\t}\n\tfor i := range po.Spec.EphemeralContainers {\n\t\tco := v1.Container(po.Spec.EphemeralContainers[i].EphemeralContainerCommon)\n\t\tres = append(res, makeContainerRes(\n\t\t\tephIDX,\n\t\t\ti,\n\t\t\t&co,\n\t\t\tpo,\n\t\t\tcmx[co.Name]),\n\t\t)\n\t}\n\n\treturn res, nil\n}\n\n// TailLogs tails a given container logs.\nfunc (c *Container) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) {\n\tpo := Pod{}\n\tpo.Init(c.Factory, client.PodGVR)\n\n\treturn po.TailLogs(ctx, opts)\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc makeContainerRes(kind string, idx int, co *v1.Container, po *v1.Pod, cmx *mv1beta1.ContainerMetrics) render.ContainerRes {\n\treturn render.ContainerRes{\n\t\tIdx:       kind + strconv.Itoa(idx+1),\n\t\tContainer: co,\n\t\tStatus:    getContainerStatus(kind, co.Name, &po.Status),\n\t\tMX:        cmx,\n\t\tAge:       po.GetCreationTimestamp(),\n\t}\n}\n\nfunc getContainerStatus(kind, name string, status *v1.PodStatus) *v1.ContainerStatus {\n\tswitch kind {\n\tcase mainIDX:\n\t\tfor i := range status.ContainerStatuses {\n\t\t\tif status.ContainerStatuses[i].Name == name {\n\t\t\t\treturn &status.ContainerStatuses[i]\n\t\t\t}\n\t\t}\n\tcase initIDX:\n\t\tfor i := range status.InitContainerStatuses {\n\t\t\tif status.InitContainerStatuses[i].Name == name {\n\t\t\t\treturn &status.InitContainerStatuses[i]\n\t\t\t}\n\t\t}\n\tcase ephIDX:\n\t\tfor i := range status.EphemeralContainerStatuses {\n\t\t\tif status.EphemeralContainerStatuses[i].Name == name {\n\t\t\t\treturn &status.EphemeralContainerStatuses[i]\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (c *Container) fetchPod(fqn string) (*v1.Pod, error) {\n\to, err := c.getFactory().Get(client.PodGVR, fqn, true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to locate pod %q: %w\", fqn, err)\n\t}\n\tvar po v1.Pod\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po)\n\treturn &po, err\n}\n"
  },
  {
    "path": "internal/dao/container_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/watch\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/version\"\n\t\"k8s.io/client-go/discovery/cached/disk\"\n\t\"k8s.io/client-go/dynamic\"\n\t\"k8s.io/client-go/informers\"\n\t\"k8s.io/client-go/kubernetes\"\n\trestclient \"k8s.io/client-go/rest\"\n\tversioned \"k8s.io/metrics/pkg/client/clientset/versioned\"\n\t\"sigs.k8s.io/yaml\"\n)\n\nfunc TestContainerList(t *testing.T) {\n\tc := dao.Container{}\n\tc.Init(makePodFactory(), client.CoGVR)\n\n\tctx := context.WithValue(context.Background(), internal.KeyPath, \"fred/p1\")\n\too, err := c.List(ctx, \"\")\n\trequire.NoError(t, err)\n\tassert.Len(t, oo, 1)\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\ntype conn struct{}\n\nfunc makeConn() *conn {\n\treturn &conn{}\n}\n\nfunc (*conn) Config() *client.Config                                   { return nil }\nfunc (*conn) Dial() (kubernetes.Interface, error)                      { return nil, nil }\nfunc (*conn) DialLogs() (kubernetes.Interface, error)                  { return nil, nil }\nfunc (*conn) ConnectionOK() bool                                       { return true }\nfunc (*conn) SwitchContext(string) error                               { return nil }\nfunc (*conn) CachedDiscovery() (*disk.CachedDiscoveryClient, error)    { return nil, nil }\nfunc (*conn) RestConfig() (*restclient.Config, error)                  { return nil, nil }\nfunc (*conn) MXDial() (*versioned.Clientset, error)                    { return nil, nil }\nfunc (*conn) DynDial() (dynamic.Interface, error)                      { return nil, nil }\nfunc (*conn) HasMetrics() bool                                         { return false }\nfunc (*conn) CheckConnectivity() bool                                  { return false }\nfunc (*conn) IsNamespaced(string) bool                                 { return false }\nfunc (*conn) SupportsResource(string) bool                             { return false }\nfunc (*conn) ValidNamespaces() ([]v1.Namespace, error)                 { return nil, nil }\nfunc (*conn) SupportsRes(string, []string) (a string, b bool, e error) { return \"\", false, nil }\nfunc (*conn) ServerVersion() (*version.Info, error)                    { return nil, nil }\nfunc (*conn) CurrentNamespaceName() (string, error)                    { return \"\", nil }\nfunc (*conn) CanI(string, *client.GVR, string, []string) (bool, error) { return true, nil }\nfunc (*conn) ActiveContext() string                                    { return \"\" }\nfunc (*conn) ActiveNamespace() string                                  { return \"\" }\nfunc (*conn) IsValidNamespace(string) bool                             { return true }\nfunc (*conn) ValidNamespaceNames() (client.NamespaceNames, error)      { return nil, nil }\nfunc (*conn) IsActiveNamespace(string) bool                            { return false }\n\ntype podFactory struct{}\n\nvar _ dao.Factory = &testFactory{}\n\nfunc (podFactory) Client() client.Connection {\n\treturn makeConn()\n}\n\nfunc (podFactory) Get(*client.GVR, string, bool, labels.Selector) (runtime.Object, error) {\n\tvar m map[string]any\n\tif err := yaml.Unmarshal([]byte(poYaml()), &m); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &unstructured.Unstructured{Object: m}, nil\n}\n\nfunc (podFactory) List(*client.GVR, string, bool, labels.Selector) ([]runtime.Object, error) {\n\treturn nil, nil\n}\nfunc (podFactory) ForResource(string, *client.GVR) (informers.GenericInformer, error) {\n\treturn nil, nil\n}\nfunc (podFactory) CanForResource(string, *client.GVR, []string) (informers.GenericInformer, error) {\n\treturn nil, nil\n}\nfunc (podFactory) WaitForCacheSync()            {}\nfunc (podFactory) Forwarders() watch.Forwarders { return nil }\nfunc (podFactory) DeleteForwarder(string)       {}\n\nfunc makePodFactory() dao.Factory {\n\treturn podFactory{}\n}\n\nfunc poYaml() string {\n\treturn `apiVersion: v1\nkind: Pod\nmetadata:\n  creationTimestamp: \"2018-12-14T17:36:43Z\"\n  labels:\n    blee: duh\n  name: fred\n  namespace: blee\nspec:\n  containers:\n  - env:\n    - name: fred\n      value: \"1\"\n      valueFrom:\n        configMapKeyRef:\n          key: blee\n    image: blee\n    name: fred\n    resources: {}\n  priority: 1\n  priorityClassName: bozo\n  volumes:\n  - hostPath:\n      path: /blee\n      type: Directory\n    name: fred\nstatus:\n  containerStatuses:\n  - image: \"\"\n    imageID: \"\"\n    lastState: {}\n    name: fred\n    ready: false\n    restartCount: 0\n    state:\n      running:\n        startedAt: null\n  phase: Running\n`\n}\n"
  },
  {
    "path": "internal/dao/context.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar (\n\t_ Accessor   = (*Context)(nil)\n\t_ Switchable = (*Context)(nil)\n)\n\n// Context represents a kubernetes context.\ntype Context struct {\n\tNonResource\n}\n\nfunc (c *Context) config() *client.Config {\n\treturn c.getFactory().Client().Config()\n}\n\n// Get a Context.\nfunc (c *Context) Get(_ context.Context, path string) (runtime.Object, error) {\n\tco, err := c.config().GetContext(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &render.NamedContext{Name: path, Context: co}, nil\n}\n\n// List all Contexts on the current cluster.\nfunc (c *Context) List(context.Context, string) ([]runtime.Object, error) {\n\tctxs, err := c.config().Contexts()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcc := make([]runtime.Object, 0, len(ctxs))\n\tfor k, v := range ctxs {\n\t\tcc = append(cc, render.NewNamedContext(c.config(), k, v))\n\t}\n\n\treturn cc, nil\n}\n\n// MustCurrentContextName return the active context name.\nfunc (c *Context) MustCurrentContextName() string {\n\tcl, err := c.config().CurrentContextName()\n\tif err != nil {\n\t\tslog.Error(\"Fetching current context\", slogs.Error, err)\n\t}\n\treturn cl\n}\n\n// Switch to another context.\nfunc (c *Context) Switch(ctx string) error {\n\treturn c.getFactory().Client().SwitchContext(ctx)\n}\n"
  },
  {
    "path": "internal/dao/crd.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nvar (\n\t_ Accessor = (*CustomResourceDefinition)(nil)\n\t_ Nuker    = (*CustomResourceDefinition)(nil)\n)\n\n// CustomResourceDefinition represents a CRD resource model.\ntype CustomResourceDefinition struct {\n\tResource\n}\n"
  },
  {
    "path": "internal/dao/cronjob.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\tbatchv1 \"k8s.io/api/batch/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/util/rand\"\n)\n\nconst maxJobNameSize = 42\n\nvar (\n\t_ Accessor    = (*CronJob)(nil)\n\t_ Runnable    = (*CronJob)(nil)\n\t_ ImageLister = (*CronJob)(nil)\n)\n\n// CronJob represents a cronjob K8s resource.\ntype CronJob struct {\n\tGeneric\n}\n\n// ListImages lists container images.\nfunc (c *CronJob) ListImages(_ context.Context, fqn string) ([]string, error) {\n\tcj, err := c.GetInstance(fqn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn render.ExtractImages(&cj.Spec.JobTemplate.Spec.Template.Spec), nil\n}\n\n// Run a CronJob.\nfunc (c *CronJob) Run(path string) error {\n\tns, n := client.Namespaced(path)\n\tauth, err := c.Client().CanI(ns, client.JobGVR, n, []string{client.GetVerb, client.CreateVerb})\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !auth {\n\t\treturn fmt.Errorf(\"user is not authorized to run jobs\")\n\t}\n\n\to, err := c.getFactory().Get(c.gvr, path, true, labels.Everything())\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar cj batchv1.CronJob\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cj)\n\tif err != nil {\n\t\treturn errors.New(\"expecting CronJob resource\")\n\t}\n\tjobName := cj.Name\n\tif len(cj.Name) >= maxJobNameSize {\n\t\tjobName = cj.Name[0:maxJobNameSize]\n\t}\n\ttrueVal := true\n\tjob := &batchv1.Job{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:        jobName + \"-manual-\" + rand.String(3),\n\t\t\tNamespace:   ns,\n\t\t\tLabels:      cj.Spec.JobTemplate.Labels,\n\t\t\tAnnotations: cj.Spec.JobTemplate.Annotations,\n\t\t\tOwnerReferences: []metav1.OwnerReference{\n\t\t\t\t{\n\t\t\t\t\tAPIVersion:         c.gvr.GV().String(),\n\t\t\t\t\tKind:               \"CronJob\",\n\t\t\t\t\tBlockOwnerDeletion: &trueVal,\n\t\t\t\t\tController:         &trueVal,\n\t\t\t\t\tName:               cj.Name,\n\t\t\t\t\tUID:                cj.UID,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tSpec: cj.Spec.JobTemplate.Spec,\n\t}\n\tdial, err := c.Client().Dial()\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), c.Client().Config().CallTimeout())\n\tdefer cancel()\n\t_, err = dial.BatchV1().Jobs(ns).Create(ctx, job, metav1.CreateOptions{})\n\n\treturn err\n}\n\n// ScanSA scans for serviceaccount refs.\nfunc (c *CronJob) ScanSA(_ context.Context, fqn string, wait bool) (Refs, error) {\n\tns, n := client.Namespaced(fqn)\n\too, err := c.getFactory().List(c.gvr, ns, wait, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trefs := make(Refs, 0, len(oo))\n\tfor _, o := range oo {\n\t\tvar cj batchv1.CronJob\n\t\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cj)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(\"expecting CronJob resource\")\n\t\t}\n\t\tif serviceAccountMatches(cj.Spec.JobTemplate.Spec.Template.Spec.ServiceAccountName, n) {\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: c.GVR(),\n\t\t\t\tFQN: client.FQN(cj.Namespace, cj.Name),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn refs, nil\n}\n\n// GetInstance fetch a matching cronjob.\nfunc (c *CronJob) GetInstance(fqn string) (*batchv1.CronJob, error) {\n\to, err := c.getFactory().Get(c.gvr, fqn, true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar cj batchv1.CronJob\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cj)\n\tif err != nil {\n\t\treturn nil, errors.New(\"expecting cronjob resource\")\n\t}\n\n\treturn &cj, nil\n}\n\n// ToggleSuspend toggles suspend/resume on a CronJob.\nfunc (c *CronJob) ToggleSuspend(ctx context.Context, path string) error {\n\tns, n := client.Namespaced(path)\n\tauth, err := c.Client().CanI(ns, c.gvr, n, []string{client.GetVerb, client.UpdateVerb})\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !auth {\n\t\treturn fmt.Errorf(\"user is not authorized to (un)suspend cronjobs\")\n\t}\n\n\tdial, err := c.Client().Dial()\n\tif err != nil {\n\t\treturn err\n\t}\n\tcj, err := dial.BatchV1().CronJobs(ns).Get(ctx, n, metav1.GetOptions{})\n\tif err != nil {\n\t\treturn err\n\t}\n\tif cj.Spec.Suspend != nil {\n\t\tcurrent := !*cj.Spec.Suspend\n\t\tcj.Spec.Suspend = &current\n\t} else {\n\t\ttrueVal := true\n\t\tcj.Spec.Suspend = &trueVal\n\t}\n\t_, err = dial.BatchV1().CronJobs(ns).Update(ctx, cj, metav1.UpdateOptions{})\n\n\treturn err\n}\n\n// Scan scans for cluster resource refs.\nfunc (c *CronJob) Scan(_ context.Context, gvr *client.GVR, fqn string, wait bool) (Refs, error) {\n\tns, n := client.Namespaced(fqn)\n\too, err := c.getFactory().List(c.gvr, ns, wait, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trefs := make(Refs, 0, len(oo))\n\tfor _, o := range oo {\n\t\tvar cj batchv1.CronJob\n\t\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cj)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(\"expecting CronJob resource\")\n\t\t}\n\t\tswitch gvr {\n\t\tcase client.CmGVR:\n\t\t\tif !hasConfigMap(&cj.Spec.JobTemplate.Spec.Template.Spec, n) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: c.GVR(),\n\t\t\t\tFQN: client.FQN(cj.Namespace, cj.Name),\n\t\t\t})\n\t\tcase client.SecGVR:\n\t\t\tfound, err := hasSecret(c.Factory, &cj.Spec.JobTemplate.Spec.Template.Spec, cj.Namespace, n, wait)\n\t\t\tif err != nil {\n\t\t\t\tslog.Warn(\"Failed to locate secret\",\n\t\t\t\t\tslogs.FQN, fqn,\n\t\t\t\t\tslogs.Error, err,\n\t\t\t\t)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !found {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: c.GVR(),\n\t\t\t\tFQN: client.FQN(cj.Namespace, cj.Name),\n\t\t\t})\n\t\tcase client.PcGVR:\n\t\t\tif !hasPC(&cj.Spec.JobTemplate.Spec.Template.Spec, n) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: c.GVR(),\n\t\t\t\tFQN: client.FQN(cj.Namespace, cj.Name),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn refs, nil\n}\n"
  },
  {
    "path": "internal/dao/cruiser.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"fmt\"\n\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nfunc mustMap(o runtime.Object, field string) map[string]any {\n\tu, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\tpanic(\"no unstructured\")\n\t}\n\tm, ok := u.Object[field].(map[string]any)\n\tif !ok {\n\t\tpanic(fmt.Sprintf(\"map extract failed for %q\", field))\n\t}\n\n\treturn m\n}\n\nfunc mustSlice(o runtime.Object, field string) []any {\n\tu, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn nil\n\t}\n\ts, ok := u.Object[field].([]any)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\treturn s\n}\n\nfunc mustField(o map[string]any, field string) any {\n\tf, ok := o[field]\n\tif !ok {\n\t\tpanic(fmt.Sprintf(\"no field for %q\", field))\n\t}\n\n\treturn f\n}\n"
  },
  {
    "path": "internal/dao/cruiser_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n)\n\nfunc TestCruiserMeta(t *testing.T) {\n\to := loadJSON(t, \"crb\")\n\n\tm := mustMap(o, \"metadata\")\n\tassert.Equal(t, \"blee\", mustField(m, \"name\"))\n}\n\nfunc TestCruiserSlice(t *testing.T) {\n\to := loadJSON(t, \"crb\")\n\n\ts := mustSlice(o, \"subjects\")\n\tassert.Len(t, s, 1)\n\tassert.Equal(t, \"fernand\", mustField(s[0].(map[string]any), \"name\"))\n\tassert.Equal(t, \"User\", mustField(s[0].(map[string]any), \"kind\"))\n}\n\n// Helpers...\n\nfunc loadJSON(t require.TestingT, n string) *unstructured.Unstructured {\n\traw, err := os.ReadFile(fmt.Sprintf(\"testdata/%s.json\", n))\n\trequire.NoError(t, err)\n\n\tvar o unstructured.Unstructured\n\terr = json.Unmarshal(raw, &o)\n\trequire.NoError(t, err)\n\n\treturn &o\n}\n"
  },
  {
    "path": "internal/dao/describe.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"log/slog\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"k8s.io/kubectl/pkg/describe\"\n)\n\n// Describe describes a resource.\nfunc Describe(c client.Connection, gvr *client.GVR, path string) (string, error) {\n\tmapper := RestMapper{Connection: c}\n\tm, err := mapper.ToRESTMapper()\n\tif err != nil {\n\t\tslog.Error(\"No REST mapper for resource\",\n\t\t\tslogs.GVR, gvr,\n\t\t\tslogs.Error, err,\n\t\t)\n\t\treturn \"\", err\n\t}\n\n\tgvk, err := m.KindFor(gvr.GVR())\n\tif err != nil {\n\t\tslog.Error(\"No GVK for resource %s\",\n\t\t\tslogs.GVR, gvr,\n\t\t\tslogs.Error, err,\n\t\t)\n\t\treturn \"\", err\n\t}\n\n\tns, n := client.Namespaced(path)\n\tif client.IsClusterScoped(ns) {\n\t\tns = client.BlankNamespace\n\t}\n\tmapping, err := mapper.ResourceFor(gvr.AsResourceName(), gvk.Kind)\n\tif err != nil {\n\t\tslog.Error(\"Unable to find mapper\",\n\t\t\tslogs.GVR, gvr,\n\t\t\tslogs.ResName, n,\n\t\t\tslogs.Error, err,\n\t\t)\n\t\treturn \"\", err\n\t}\n\td, err := describe.Describer(c.Config().Flags(), mapping)\n\tif err != nil {\n\t\tslog.Error(\"Unable to find describer\",\n\t\t\tslogs.GVR, gvr.AsResourceName(),\n\t\t\tslogs.Error, err,\n\t\t)\n\t\treturn \"\", err\n\t}\n\n\treturn d.Describe(ns, n, describe.DescriberSettings{ShowEvents: true})\n}\n"
  },
  {
    "path": "internal/dao/dir.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar _ Accessor = (*Dir)(nil)\n\n// Dir tracks standard and custom command aliases.\ntype Dir struct {\n\tNonResource\n}\n\n// NewDir returns a new set of aliases.\nfunc NewDir(f Factory) *Dir {\n\tvar a Dir\n\ta.Init(f, client.DirGVR)\n\treturn &a\n}\n\nvar yamlRX = regexp.MustCompile(`.*\\.(yml|yaml|json)`)\n\n// List returns a collection of aliases.\nfunc (*Dir) List(ctx context.Context, _ string) ([]runtime.Object, error) {\n\tdir, ok := ctx.Value(internal.KeyPath).(string)\n\tif !ok {\n\t\treturn nil, errors.New(\"no dir in context\")\n\t}\n\n\tfiles, err := os.ReadDir(dir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\too := make([]runtime.Object, 0, len(files))\n\tfor _, f := range files {\n\t\tif strings.HasPrefix(f.Name(), \".\") || !f.IsDir() && !yamlRX.MatchString(f.Name()) {\n\t\t\tcontinue\n\t\t}\n\t\too = append(oo, render.DirRes{\n\t\t\tPath:  filepath.Join(dir, f.Name()),\n\t\t\tEntry: f,\n\t\t})\n\t}\n\n\treturn oo, err\n}\n\n// Get fetch a resource.\nfunc (*Dir) Get(_ context.Context, _ string) (runtime.Object, error) {\n\treturn nil, errors.New(\"nyi\")\n}\n"
  },
  {
    "path": "internal/dao/dir_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewDir(t *testing.T) {\n\td := dao.NewDir(nil)\n\tctx := context.WithValue(context.Background(), internal.KeyPath, \"testdata/dir\")\n\too, err := d.List(ctx, \"\")\n\n\trequire.NoError(t, err)\n\tassert.Len(t, oo, 2)\n}\n"
  },
  {
    "path": "internal/dao/dp.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/types\"\n\t\"k8s.io/apimachinery/pkg/util/strategicpatch\"\n\t\"k8s.io/kubectl/pkg/polymorphichelpers\"\n\t\"k8s.io/kubectl/pkg/scheme\"\n)\n\nvar (\n\t_ Accessor        = (*Deployment)(nil)\n\t_ Nuker           = (*Deployment)(nil)\n\t_ Loggable        = (*Deployment)(nil)\n\t_ Restartable     = (*Deployment)(nil)\n\t_ Scalable        = (*Deployment)(nil)\n\t_ Controller      = (*Deployment)(nil)\n\t_ ContainsPodSpec = (*Deployment)(nil)\n\t_ ImageLister     = (*Deployment)(nil)\n)\n\n// Deployment represents a deployment K8s resource.\ntype Deployment struct {\n\tResource\n}\n\n// ListImages lists container images.\nfunc (d *Deployment) ListImages(_ context.Context, fqn string) ([]string, error) {\n\tdp, err := d.GetInstance(fqn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn render.ExtractImages(&dp.Spec.Template.Spec), nil\n}\n\n// Scale a Deployment.\nfunc (d *Deployment) Scale(ctx context.Context, path string, replicas int32) error {\n\treturn scaleRes(ctx, d.getFactory(), client.DpGVR, path, replicas)\n}\n\n// Restart a Deployment rollout.\nfunc (d *Deployment) Restart(ctx context.Context, path string, opts *metav1.PatchOptions) error {\n\treturn restartRes[*appsv1.Deployment](ctx, d.getFactory(), client.DpGVR, path, opts)\n}\n\n// TailLogs tail logs for all pods represented by this Deployment.\nfunc (d *Deployment) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) {\n\tdp, err := d.GetInstance(opts.Path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif dp.Spec.Selector == nil || len(dp.Spec.Selector.MatchLabels) == 0 {\n\t\treturn nil, fmt.Errorf(\"no valid selector found on deployment: %s\", opts.Path)\n\t}\n\n\treturn podLogs(ctx, dp.Spec.Selector.MatchLabels, opts)\n}\n\n// Pod returns a pod victim by name.\nfunc (d *Deployment) Pod(fqn string) (string, error) {\n\tdp, err := d.GetInstance(fqn)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn podFromSelector(d.Factory, dp.Namespace, dp.Spec.Selector.MatchLabels)\n}\n\n// GetInstance fetch a matching deployment.\nfunc (d *Deployment) GetInstance(fqn string) (*appsv1.Deployment, error) {\n\to, err := d.Factory.Get(d.gvr, fqn, true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar dp appsv1.Deployment\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp)\n\tif err != nil {\n\t\treturn nil, errors.New(\"expecting Deployment resource\")\n\t}\n\n\treturn &dp, nil\n}\n\n// ScanSA scans for serviceaccount refs.\nfunc (d *Deployment) ScanSA(_ context.Context, fqn string, wait bool) (Refs, error) {\n\tns, n := client.Namespaced(fqn)\n\too, err := d.getFactory().List(d.gvr, ns, wait, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trefs := make(Refs, 0, len(oo))\n\tfor _, o := range oo {\n\t\tvar dp appsv1.Deployment\n\t\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(\"expecting Deployment resource\")\n\t\t}\n\t\tif serviceAccountMatches(dp.Spec.Template.Spec.ServiceAccountName, n) {\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: d.GVR(),\n\t\t\t\tFQN: client.FQN(dp.Namespace, dp.Name),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn refs, nil\n}\n\n// Scan scans for resource references.\nfunc (d *Deployment) Scan(_ context.Context, gvr *client.GVR, fqn string, wait bool) (Refs, error) {\n\tns, n := client.Namespaced(fqn)\n\too, err := d.getFactory().List(d.gvr, ns, wait, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trefs := make(Refs, 0, len(oo))\n\tfor _, o := range oo {\n\t\tvar dp appsv1.Deployment\n\t\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &dp)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(\"expecting Deployment resource\")\n\t\t}\n\t\tswitch gvr {\n\t\tcase client.CmGVR:\n\t\t\tif !hasConfigMap(&dp.Spec.Template.Spec, n) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: d.GVR(),\n\t\t\t\tFQN: client.FQN(dp.Namespace, dp.Name),\n\t\t\t})\n\t\tcase client.SecGVR:\n\t\t\tfound, err := hasSecret(d.Factory, &dp.Spec.Template.Spec, dp.Namespace, n, wait)\n\t\t\tif err != nil {\n\t\t\t\tslog.Warn(\"Fail to locate secret\",\n\t\t\t\t\tslogs.FQN, fqn,\n\t\t\t\t\tslogs.Error, err,\n\t\t\t\t)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !found {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: d.GVR(),\n\t\t\t\tFQN: client.FQN(dp.Namespace, dp.Name),\n\t\t\t})\n\t\tcase client.PvcGVR:\n\t\t\tif !hasPVC(&dp.Spec.Template.Spec, n) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: d.GVR(),\n\t\t\t\tFQN: client.FQN(dp.Namespace, dp.Name),\n\t\t\t})\n\t\tcase client.PcGVR:\n\t\t\tif !hasPC(&dp.Spec.Template.Spec, n) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: d.GVR(),\n\t\t\t\tFQN: client.FQN(dp.Namespace, dp.Name),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn refs, nil\n}\n\n// GetPodSpec returns a pod spec given a resource.\nfunc (d *Deployment) GetPodSpec(path string) (*v1.PodSpec, error) {\n\tdp, err := d.GetInstance(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpodSpec := dp.Spec.Template.Spec\n\treturn &podSpec, nil\n}\n\n// SetImages sets container images.\nfunc (d *Deployment) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {\n\tns, n := client.Namespaced(path)\n\tauth, err := d.Client().CanI(ns, d.gvr, n, client.PatchAccess)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !auth {\n\t\treturn fmt.Errorf(\"user is not authorized to patch a deployment\")\n\t}\n\tjsonPatch, err := GetTemplateJsonPatch(imageSpecs)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdial, err := d.Client().Dial()\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = dial.AppsV1().Deployments(ns).Patch(\n\t\tctx,\n\t\tn,\n\t\ttypes.StrategicMergePatchType,\n\t\tjsonPatch,\n\t\tmetav1.PatchOptions{},\n\t)\n\treturn err\n}\n\n// Helpers...\n\nfunc hasPVC(spec *v1.PodSpec, name string) bool {\n\tfor i := range spec.Volumes {\n\t\tif spec.Volumes[i].PersistentVolumeClaim != nil && spec.Volumes[i].PersistentVolumeClaim.ClaimName == name {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc hasPC(spec *v1.PodSpec, name string) bool {\n\treturn spec.PriorityClassName == name\n}\n\nfunc hasConfigMap(spec *v1.PodSpec, name string) bool {\n\tfor i := range spec.InitContainers {\n\t\tif containerHasConfigMap(spec.InitContainers[i].EnvFrom, spec.InitContainers[i].Env, name) {\n\t\t\treturn true\n\t\t}\n\t}\n\tfor i := range spec.Containers {\n\t\tif containerHasConfigMap(spec.Containers[i].EnvFrom, spec.Containers[i].Env, name) {\n\t\t\treturn true\n\t\t}\n\t}\n\tfor i := range spec.EphemeralContainers {\n\t\tif containerHasConfigMap(spec.EphemeralContainers[i].EnvFrom, spec.EphemeralContainers[i].Env, name) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tfor i := range spec.Volumes {\n\t\tif cm := spec.Volumes[i].ConfigMap; cm != nil {\n\t\t\tif cm.Name == name {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\nfunc hasSecret(f Factory, spec *v1.PodSpec, ns, name string, wait bool) (bool, error) {\n\tfor i := range spec.InitContainers {\n\t\tif containerHasSecret(spec.InitContainers[i].EnvFrom, spec.InitContainers[i].Env, name) {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\tfor i := range spec.Containers {\n\t\tif containerHasSecret(spec.Containers[i].EnvFrom, spec.Containers[i].Env, name) {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\tfor i := range spec.EphemeralContainers {\n\t\tif containerHasSecret(spec.EphemeralContainers[i].EnvFrom, spec.EphemeralContainers[i].Env, name) {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\tfor _, s := range spec.ImagePullSecrets {\n\t\tif s.Name == name {\n\t\t\treturn true, nil\n\t\t}\n\t}\n\n\tif saName := spec.ServiceAccountName; saName != \"\" {\n\t\to, err := f.Get(client.SaGVR, client.FQN(ns, saName), wait, labels.Everything())\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tvar sa v1.ServiceAccount\n\t\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &sa)\n\t\tif err != nil {\n\t\t\treturn false, errors.New(\"expecting ServiceAccount resource\")\n\t\t}\n\n\t\tfor _, ref := range sa.Secrets {\n\t\t\tif ref.Namespace == ns && ref.Name == name {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t}\n\t}\n\n\tfor i := range spec.Volumes {\n\t\tif sec := spec.Volumes[i].Secret; sec != nil {\n\t\t\tif sec.SecretName == name {\n\t\t\t\treturn true, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false, nil\n}\n\nfunc containerHasSecret(envFrom []v1.EnvFromSource, env []v1.EnvVar, name string) bool {\n\tfor _, e := range envFrom {\n\t\tif e.SecretRef != nil && e.SecretRef.Name == name {\n\t\t\treturn true\n\t\t}\n\t}\n\tfor _, e := range env {\n\t\tif e.ValueFrom == nil || e.ValueFrom.SecretKeyRef == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif e.ValueFrom.SecretKeyRef.Name == name {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc containerHasConfigMap(envFrom []v1.EnvFromSource, env []v1.EnvVar, name string) bool {\n\tfor _, e := range envFrom {\n\t\tif e.ConfigMapRef != nil && e.ConfigMapRef.Name == name {\n\t\t\treturn true\n\t\t}\n\t}\n\tfor _, e := range env {\n\t\tif e.ValueFrom == nil || e.ValueFrom.ConfigMapKeyRef == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif e.ValueFrom.ConfigMapKeyRef.Name == name {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc scaleRes(ctx context.Context, f Factory, gvr *client.GVR, path string, replicas int32) error {\n\tns, n := client.Namespaced(path)\n\tauth, err := f.Client().CanI(ns, client.NewGVR(gvr.String()+\":scale\"), n, []string{client.GetVerb, client.UpdateVerb})\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !auth {\n\t\treturn fmt.Errorf(\"user is not authorized to scale: %s\", gvr)\n\t}\n\n\tdial, err := f.Client().Dial()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch gvr {\n\tcase client.DpGVR:\n\t\tscale, e := dial.AppsV1().Deployments(ns).GetScale(ctx, n, metav1.GetOptions{})\n\t\tif e != nil {\n\t\t\treturn e\n\t\t}\n\t\tscale.Spec.Replicas = replicas\n\t\t_, e = dial.AppsV1().Deployments(ns).UpdateScale(ctx, n, scale, metav1.UpdateOptions{})\n\t\treturn e\n\tcase client.StsGVR:\n\t\tscale, e := dial.AppsV1().StatefulSets(ns).GetScale(ctx, n, metav1.GetOptions{})\n\t\tif e != nil {\n\t\t\treturn e\n\t\t}\n\t\tscale.Spec.Replicas = replicas\n\t\t_, e = dial.AppsV1().StatefulSets(ns).UpdateScale(ctx, n, scale, metav1.UpdateOptions{})\n\t\treturn e\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported resource for scaling: %s\", gvr)\n\t}\n}\n\nfunc restartRes[T runtime.Object](ctx context.Context, f Factory, gvr *client.GVR, path string, opts *metav1.PatchOptions) error {\n\to, err := f.Get(gvr, path, true, labels.Everything())\n\tif err != nil {\n\t\treturn err\n\t}\n\tvar r = new(T)\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tns, n := client.Namespaced(path)\n\tauth, err := f.Client().CanI(ns, gvr, n, client.PatchAccess)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !auth {\n\t\treturn fmt.Errorf(\"user is not authorized to restart %q\", gvr)\n\t}\n\n\tdial, err := f.Client().Dial()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tbefore, err := runtime.Encode(scheme.Codecs.LegacyCodec(appsv1.SchemeGroupVersion), *r)\n\tif err != nil {\n\t\treturn err\n\t}\n\tafter, err := polymorphichelpers.ObjectRestarterFn(*r)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdiff, err := strategicpatch.CreateTwoWayMergePatch(before, after, *r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch gvr {\n\tcase client.DpGVR:\n\t\t_, err = dial.AppsV1().Deployments(ns).Patch(\n\t\t\tctx,\n\t\t\tn,\n\t\t\ttypes.StrategicMergePatchType,\n\t\t\tdiff,\n\t\t\t*opts,\n\t\t)\n\n\tcase client.DsGVR:\n\t\t_, err = dial.AppsV1().DaemonSets(ns).Patch(\n\t\t\tctx,\n\t\t\tn,\n\t\t\ttypes.StrategicMergePatchType,\n\t\t\tdiff,\n\t\t\t*opts,\n\t\t)\n\n\tcase client.StsGVR:\n\t\t_, err = dial.AppsV1().StatefulSets(ns).Patch(\n\t\t\tctx,\n\t\t\tn,\n\t\t\ttypes.StrategicMergePatchType,\n\t\t\tdiff,\n\t\t\t*opts,\n\t\t)\n\t}\n\n\treturn err\n}\n"
  },
  {
    "path": "internal/dao/ds.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/watch\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/types\"\n)\n\nvar (\n\t_ Accessor        = (*DaemonSet)(nil)\n\t_ Nuker           = (*DaemonSet)(nil)\n\t_ Loggable        = (*DaemonSet)(nil)\n\t_ Restartable     = (*DaemonSet)(nil)\n\t_ Controller      = (*DaemonSet)(nil)\n\t_ ContainsPodSpec = (*DaemonSet)(nil)\n\t_ ImageLister     = (*DaemonSet)(nil)\n)\n\n// DaemonSet represents a K8s daemonset.\ntype DaemonSet struct {\n\tResource\n}\n\n// ListImages lists container images.\nfunc (d *DaemonSet) ListImages(_ context.Context, fqn string) ([]string, error) {\n\tds, err := d.GetInstance(fqn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn render.ExtractImages(&ds.Spec.Template.Spec), nil\n}\n\n// Restart a DaemonSet rollout.\nfunc (d *DaemonSet) Restart(ctx context.Context, path string, opts *metav1.PatchOptions) error {\n\treturn restartRes[*appsv1.DaemonSet](ctx, d.getFactory(), client.DsGVR, path, opts)\n}\n\n// TailLogs tail logs for all pods represented by this DaemonSet.\nfunc (d *DaemonSet) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) {\n\tds, err := d.GetInstance(opts.Path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif ds.Spec.Selector == nil || len(ds.Spec.Selector.MatchLabels) == 0 {\n\t\treturn nil, fmt.Errorf(\"no valid selector found on daemonset %q\", opts.Path)\n\t}\n\n\treturn podLogs(ctx, ds.Spec.Selector.MatchLabels, opts)\n}\n\nfunc podLogs(ctx context.Context, sel map[string]string, opts *LogOptions) ([]LogChan, error) {\n\tf, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)\n\tif !ok {\n\t\treturn nil, errors.New(\"expecting a context factory\")\n\t}\n\tls, err := metav1.ParseToLabelSelector(toSelector(sel))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlsel, err := metav1.LabelSelectorAsSelector(ls)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tns, _ := client.Namespaced(opts.Path)\n\too, err := f.List(client.PodGVR, ns, true, lsel)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\topts.MultiPods = true\n\n\tvar po Pod\n\tpo.Init(f, client.PodGVR)\n\n\touts := make([]LogChan, 0, len(oo))\n\tfor _, o := range oo {\n\t\tu, ok := o.(*unstructured.Unstructured)\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"expected unstructured got %t\", o)\n\t\t}\n\t\topts = opts.Clone()\n\t\topts.Path = client.FQN(u.GetNamespace(), u.GetName())\n\t\tcc, err := po.TailLogs(ctx, opts)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\touts = append(outs, cc...)\n\t}\n\n\treturn outs, nil\n}\n\n// Pod returns a pod victim by name.\nfunc (d *DaemonSet) Pod(fqn string) (string, error) {\n\tds, err := d.GetInstance(fqn)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn podFromSelector(d.Factory, ds.Namespace, ds.Spec.Selector.MatchLabels)\n}\n\n// GetInstance returns a daemonset instance.\nfunc (d *DaemonSet) GetInstance(fqn string) (*appsv1.DaemonSet, error) {\n\to, err := d.getFactory().Get(d.gvr, fqn, true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar ds appsv1.DaemonSet\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds)\n\tif err != nil {\n\t\treturn nil, errors.New(\"expecting DaemonSet resource\")\n\t}\n\n\treturn &ds, nil\n}\n\n// ScanSA scans for serviceaccount refs.\nfunc (d *DaemonSet) ScanSA(_ context.Context, fqn string, wait bool) (Refs, error) {\n\tns, n := client.Namespaced(fqn)\n\too, err := d.getFactory().List(d.gvr, ns, wait, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trefs := make(Refs, 0, len(oo))\n\tfor _, o := range oo {\n\t\tvar ds appsv1.DaemonSet\n\t\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(\"expecting DaemonSet resource\")\n\t\t}\n\t\tif serviceAccountMatches(ds.Spec.Template.Spec.ServiceAccountName, n) {\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: d.GVR(),\n\t\t\t\tFQN: client.FQN(ds.Namespace, ds.Name),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn refs, nil\n}\n\n// Scan scans for cluster refs.\nfunc (d *DaemonSet) Scan(_ context.Context, gvr *client.GVR, fqn string, wait bool) (Refs, error) {\n\tns, n := client.Namespaced(fqn)\n\too, err := d.getFactory().List(d.gvr, ns, wait, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trefs := make(Refs, 0, len(oo))\n\tfor _, o := range oo {\n\t\tvar ds appsv1.DaemonSet\n\t\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ds)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(\"expecting StatefulSet resource\")\n\t\t}\n\t\tswitch gvr {\n\t\tcase client.CmGVR:\n\t\t\tif !hasConfigMap(&ds.Spec.Template.Spec, n) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: d.GVR(),\n\t\t\t\tFQN: client.FQN(ds.Namespace, ds.Name),\n\t\t\t})\n\t\tcase client.SecGVR:\n\t\t\tfound, err := hasSecret(d.Factory, &ds.Spec.Template.Spec, ds.Namespace, n, wait)\n\t\t\tif err != nil {\n\t\t\t\tslog.Warn(\"Unable to locate secret\",\n\t\t\t\t\tslogs.FQN, fqn,\n\t\t\t\t\tslogs.Error, err,\n\t\t\t\t)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !found {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: d.GVR(),\n\t\t\t\tFQN: client.FQN(ds.Namespace, ds.Name),\n\t\t\t})\n\t\tcase client.PvcGVR:\n\t\t\tif !hasPVC(&ds.Spec.Template.Spec, n) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: d.GVR(),\n\t\t\t\tFQN: client.FQN(ds.Namespace, ds.Name),\n\t\t\t})\n\t\tcase client.PcGVR:\n\t\t\tif !hasPC(&ds.Spec.Template.Spec, n) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: d.GVR(),\n\t\t\t\tFQN: client.FQN(ds.Namespace, ds.Name),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn refs, nil\n}\n\n// GetPodSpec returns a pod spec given a resource.\nfunc (d *DaemonSet) GetPodSpec(path string) (*v1.PodSpec, error) {\n\tds, err := d.GetInstance(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpodSpec := ds.Spec.Template.Spec\n\treturn &podSpec, nil\n}\n\n// SetImages sets container images.\nfunc (d *DaemonSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {\n\tns, n := client.Namespaced(path)\n\tauth, err := d.Client().CanI(ns, d.gvr, n, client.PatchAccess)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !auth {\n\t\treturn fmt.Errorf(\"user is not authorized to patch a daemonset\")\n\t}\n\tjsonPatch, err := GetTemplateJsonPatch(imageSpecs)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdial, err := d.Client().Dial()\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = dial.AppsV1().DaemonSets(ns).Patch(\n\t\tctx,\n\t\tn,\n\t\ttypes.StrategicMergePatchType,\n\t\tjsonPatch,\n\t\tmetav1.PatchOptions{},\n\t)\n\treturn err\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc toSelector(m map[string]string) string {\n\ts := make([]string, 0, len(m))\n\tfor k, v := range m {\n\t\ts = append(s, k+\"=\"+v)\n\t}\n\n\treturn strings.Join(s, \",\")\n}\n"
  },
  {
    "path": "internal/dao/dynamic.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\npackage dao\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"k8s.io/apimachinery/pkg/api/meta\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\tmetav1beta1 \"k8s.io/apimachinery/pkg/apis/meta/v1beta1\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n\t\"k8s.io/client-go/rest\"\n\tcmdutil \"k8s.io/kubectl/pkg/cmd/util\"\n)\n\ntype Dynamic struct {\n\tGeneric\n}\n\n// Get returns a given resource as a table object.\nfunc (d *Dynamic) Get(ctx context.Context, path string) (runtime.Object, error) {\n\too, err := d.toTable(ctx, path)\n\tif err != nil || len(oo) == 0 {\n\t\treturn nil, err\n\t}\n\n\treturn oo[0], nil\n}\n\n// List returns a collection of resources as one or more table objects.\nfunc (d *Dynamic) List(ctx context.Context, ns string) ([]runtime.Object, error) {\n\treturn d.toTable(ctx, ns+\"/\")\n}\n\nfunc (d *Dynamic) toTable(ctx context.Context, fqn string) ([]runtime.Object, error) {\n\tsel := labels.Everything()\n\tif s, ok := ctx.Value(internal.KeyLabels).(labels.Selector); ok {\n\t\tsel = s\n\t}\n\n\topts := []string{d.gvr.AsResourceName()}\n\tns, n := client.Namespaced(fqn)\n\tif n != \"\" {\n\t\topts = append(opts, n)\n\t}\n\tallNS := client.IsAllNamespaces(ns)\n\tflags := cmdutil.NewMatchVersionFlags(d.getFactory().Client().Config().Flags())\n\tf := cmdutil.NewFactory(flags)\n\tb := f.NewBuilder().\n\t\tUnstructured().\n\t\tNamespaceParam(ns).DefaultNamespace().AllNamespaces(allNS).\n\t\tLabelSelectorParam(sel.String()).\n\t\tFieldSelectorParam(\"\").\n\t\tRequestChunksOf(0).\n\t\tResourceTypeOrNameArgs(true, opts...).\n\t\tContinueOnError().\n\t\tLatest().\n\t\tFlatten().\n\t\tTransformRequests(d.transformRequests).\n\t\tDo()\n\tif err := b.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tinfos, err := b.Infos()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\too := make([]runtime.Object, 0, len(infos))\n\tfor _, info := range infos {\n\t\to, err := decodeIntoTable(info.Object, allNS)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\too = append(oo, o.(*metav1.Table))\n\t}\n\n\treturn oo, nil\n}\n\nvar recognizedTableVersions = map[schema.GroupVersionKind]bool{\n\tmetav1beta1.SchemeGroupVersion.WithKind(\"Table\"): true,\n\tmetav1.SchemeGroupVersion.WithKind(\"Table\"):      true,\n}\n\nfunc decodeIntoTable(obj runtime.Object, allNs bool) (runtime.Object, error) {\n\tevent, isEvent := obj.(*metav1.WatchEvent)\n\tif isEvent {\n\t\tobj = event.Object.Object\n\t}\n\tif !recognizedTableVersions[obj.GetObjectKind().GroupVersionKind()] {\n\t\treturn nil, fmt.Errorf(\"attempt to decode non-Table object: %v\", obj.GetObjectKind().GroupVersionKind())\n\t}\n\n\tu, ok := obj.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"attempt to decode non-Unstructured object\")\n\t}\n\tvar table metav1.Table\n\tif err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &table); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif allNs {\n\t\tdefs := make([]metav1.TableColumnDefinition, 0, len(table.ColumnDefinitions)+1)\n\t\tdefs = append(defs, metav1.TableColumnDefinition{Name: \"Namespace\", Type: \"string\"})\n\t\tdefs = append(defs, table.ColumnDefinitions...)\n\t\ttable.ColumnDefinitions = defs\n\t}\n\n\tfor i := range table.Rows {\n\t\trow := &table.Rows[i]\n\t\tif row.Object.Raw == nil || row.Object.Object != nil {\n\t\t\tcontinue\n\t\t}\n\t\tconverted, err := runtime.Decode(unstructured.UnstructuredJSONScheme, row.Object.Raw)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trow.Object.Object = converted\n\t\tvar m metav1.Object\n\t\tif obj := row.Object.Object; obj != nil {\n\t\t\tm, _ = meta.Accessor(obj)\n\t\t}\n\t\tvar ns string\n\t\tif m != nil {\n\t\t\tns = m.GetNamespace()\n\t\t}\n\t\tif allNs {\n\t\t\tcells := make([]any, 0, len(row.Cells)+1)\n\t\t\tcells = append(cells, ns)\n\t\t\tcells = append(cells, row.Cells...)\n\t\t\trow.Cells = cells\n\t\t}\n\t}\n\n\tif isEvent {\n\t\tevent.Object.Object = &table\n\t\treturn event, nil\n\t}\n\n\treturn &table, nil\n}\n\nfunc (d *Dynamic) transformRequests(req *rest.Request) {\n\treq.SetHeader(\"Accept\", strings.Join([]string{\n\t\tfmt.Sprintf(\"application/json;as=Table;v=%s;g=%s\", metav1.SchemeGroupVersion.Version, metav1.GroupName),\n\t\tfmt.Sprintf(\"application/json;as=Table;v=%s;g=%s\", metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName),\n\t\t\"application/json\",\n\t}, \",\"))\n\n\tif d.includeObj {\n\t\treq.Param(\"includeObject\", \"Object\")\n\t}\n}\n"
  },
  {
    "path": "internal/dao/generic.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/client-go/dynamic\"\n)\n\ntype Grace int64\n\nconst (\n\t// DefaultGrace uses delete default termination policy.\n\tDefaultGrace Grace = -1\n\n\t// ForceGrace sets delete grace-period to 0.\n\tForceGrace Grace = 0\n\n\t// NowGrace set delete grace-period to 1,\n\tNowGrace Grace = 1\n)\n\nvar _ Describer = (*Generic)(nil)\n\n// Generic represents a generic resource.\ntype Generic struct {\n\tNonResource\n}\n\n// List returns a collection of resources.\n// BOZO!! no auth check??\nfunc (g *Generic) List(ctx context.Context, ns string) ([]runtime.Object, error) {\n\tlabelSel, ok := ctx.Value(internal.KeyLabels).(labels.Selector)\n\tif !ok {\n\t\tlabelSel = labels.Everything()\n\t}\n\tif client.IsAllNamespace(ns) {\n\t\tns = client.BlankNamespace\n\t}\n\n\tdial, err := g.dynClient()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\topts := metav1.ListOptions{LabelSelector: labelSel.String()}\n\tvar ll *unstructured.UnstructuredList\n\tif client.IsClusterScoped(ns) {\n\t\tll, err = dial.List(ctx, opts)\n\t} else {\n\t\tll, err = dial.Namespace(ns).List(ctx, opts)\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\too := make([]runtime.Object, len(ll.Items))\n\tfor i := range ll.Items {\n\t\too[i] = &ll.Items[i]\n\t}\n\n\treturn oo, nil\n}\n\n// Get returns a given resource.\nfunc (g *Generic) Get(ctx context.Context, path string) (runtime.Object, error) {\n\tns, n := client.Namespaced(path)\n\tdial, err := g.dynClient()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar opts metav1.GetOptions\n\tif client.IsClusterScoped(ns) {\n\t\treturn dial.Get(ctx, n, opts)\n\t}\n\n\treturn dial.Namespace(ns).Get(ctx, n, opts)\n}\n\n// Describe describes a resource.\nfunc (g *Generic) Describe(path string) (string, error) {\n\treturn Describe(g.Client(), g.gvr, path)\n}\n\n// ToYAML returns a resource yaml.\nfunc (g *Generic) ToYAML(path string, showManaged bool) (string, error) {\n\to, err := g.Get(context.Background(), path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\traw, err := ToYAML(o, showManaged)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to marshal resource %w\", err)\n\t}\n\treturn raw, nil\n}\n\n// Delete deletes a resource.\nfunc (g *Generic) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error {\n\tns, n := client.Namespaced(path)\n\tauth, err := g.Client().CanI(ns, g.gvr, n, []string{client.DeleteVerb})\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !auth {\n\t\treturn fmt.Errorf(\"user is not authorized to delete %s\", path)\n\t}\n\n\tvar gracePeriod *int64\n\tif grace != DefaultGrace {\n\t\tgracePeriod = (*int64)(&grace)\n\t}\n\topts := metav1.DeleteOptions{\n\t\tPropagationPolicy:  propagation,\n\t\tGracePeriodSeconds: gracePeriod,\n\t}\n\n\tdial, err := g.dynClient()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif client.IsClusterScoped(ns) {\n\t\treturn dial.Delete(ctx, n, opts)\n\t}\n\tctx, cancel := context.WithTimeout(ctx, g.Client().Config().CallTimeout())\n\tdefer cancel()\n\n\treturn dial.Namespace(ns).Delete(ctx, n, opts)\n}\n\nfunc (g *Generic) dynClient() (dynamic.NamespaceableResourceInterface, error) {\n\tdial, err := g.Client().DynDial()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn dial.Resource(g.gvr.GVR()), nil\n}\n"
  },
  {
    "path": "internal/dao/helm_chart.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/render/helm\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"helm.sh/helm/v3/pkg/action\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/cli-runtime/pkg/genericclioptions\"\n)\n\nvar (\n\t_ Accessor  = (*HelmChart)(nil)\n\t_ Nuker     = (*HelmChart)(nil)\n\t_ Describer = (*HelmChart)(nil)\n\t_ Valuer    = (*HelmChart)(nil)\n)\n\n// HelmChart represents a helm chart.\ntype HelmChart struct {\n\tNonResource\n}\n\n// List returns a collection of resources.\nfunc (h *HelmChart) List(_ context.Context, ns string) ([]runtime.Object, error) {\n\tcfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlist := action.NewList(cfg)\n\tlist.All = true\n\tlist.SetStateMask()\n\trr, err := list.Run()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\too := make([]runtime.Object, 0, len(rr))\n\tfor _, r := range rr {\n\t\too = append(oo, helm.ReleaseRes{Release: r})\n\t}\n\n\treturn oo, nil\n}\n\n// Get returns a resource.\nfunc (h *HelmChart) Get(_ context.Context, path string) (runtime.Object, error) {\n\tns, n := client.Namespaced(path)\n\tcfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresp, err := action.NewGet(cfg).Run(n)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn helm.ReleaseRes{Release: resp}, nil\n}\n\n// GetValues returns values for a release\nfunc (h *HelmChart) GetValues(path string, allValues bool) ([]byte, error) {\n\tns, n := client.Namespaced(path)\n\tcfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvals := action.NewGetValues(cfg)\n\tvals.AllValues = allValues\n\tresp, err := vals.Run(n)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn data.WriteYAML(resp)\n}\n\n// Describe returns the chart notes.\nfunc (h *HelmChart) Describe(path string) (string, error) {\n\tns, n := client.Namespaced(path)\n\tcfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tresp, err := action.NewGet(cfg).Run(n)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn resp.Info.Notes, nil\n}\n\n// ToYAML returns the chart manifest.\nfunc (h *HelmChart) ToYAML(path string, _ bool) (string, error) {\n\tns, n := client.Namespaced(path)\n\tcfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tresp, err := action.NewGet(cfg).Run(n)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn resp.Manifest, nil\n}\n\n// Delete uninstall a HelmChart.\nfunc (h *HelmChart) Delete(_ context.Context, path string, _ *metav1.DeletionPropagation, _ Grace) error {\n\treturn h.Uninstall(path, false)\n}\n\n// Uninstall uninstalls a HelmChart.\nfunc (h *HelmChart) Uninstall(path string, keepHist bool) error {\n\tns, n := client.Namespaced(path)\n\tflags := h.Client().Config().Flags()\n\tcfg, err := ensureHelmConfig(flags, ns)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tu := action.NewUninstall(cfg)\n\tu.KeepHistory = keepHist\n\tres, err := u.Run(n)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif res != nil && res.Info != \"\" {\n\t\treturn fmt.Errorf(\"%s\", res.Info)\n\t}\n\n\treturn nil\n}\n\n// ensureHelmConfig return a new configuration.\nfunc ensureHelmConfig(flags *genericclioptions.ConfigFlags, ns string) (*action.Configuration, error) {\n\tsettings := &genericclioptions.ConfigFlags{\n\t\tNamespace:        &ns,\n\t\tContext:          flags.Context,\n\t\tBearerToken:      flags.BearerToken,\n\t\tAPIServer:        flags.APIServer,\n\t\tCAFile:           flags.CAFile,\n\t\tKubeConfig:       flags.KubeConfig,\n\t\tImpersonate:      flags.Impersonate,\n\t\tInsecure:         flags.Insecure,\n\t\tTLSServerName:    flags.TLSServerName,\n\t\tImpersonateGroup: flags.ImpersonateGroup,\n\t\tWrapConfigFn:     flags.WrapConfigFn,\n\t}\n\tcfg := new(action.Configuration)\n\terr := cfg.Init(settings, ns, os.Getenv(\"HELM_DRIVER\"), helmLogger)\n\n\treturn cfg, err\n}\n\nfunc helmLogger(fmat string, args ...any) {\n\tslog.Debug(\"Log\",\n\t\tslogs.Log, fmt.Sprintf(fmat, args...),\n\t\tslogs.Subsys, \"helm\",\n\t)\n}\n"
  },
  {
    "path": "internal/dao/helm_history.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/render/helm\"\n\t\"helm.sh/helm/v3/pkg/action\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar (\n\t_ Accessor  = (*HelmHistory)(nil)\n\t_ Nuker     = (*HelmHistory)(nil)\n\t_ Describer = (*HelmHistory)(nil)\n\t_ Valuer    = (*HelmHistory)(nil)\n)\n\n// HelmHistory represents a helm chart.\ntype HelmHistory struct {\n\tNonResource\n}\n\n// List returns a collection of resources.\nfunc (h *HelmHistory) List(ctx context.Context, _ string) ([]runtime.Object, error) {\n\tpath, ok := ctx.Value(internal.KeyFQN).(string)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"expecting FQN in context\")\n\t}\n\tns, n := client.Namespaced(path)\n\n\tcfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\thh, err := action.NewHistory(cfg).Run(n)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\too := make([]runtime.Object, 0, len(hh))\n\tfor _, r := range hh {\n\t\too = append(oo, helm.ReleaseRes{Release: r})\n\t}\n\n\treturn oo, nil\n}\n\n// Get returns a resource.\nfunc (h *HelmHistory) Get(_ context.Context, path string) (runtime.Object, error) {\n\tfqn, rev, found := strings.Cut(path, \":\")\n\tif !found || rev == \"\" {\n\t\treturn nil, fmt.Errorf(\"invalid path %q\", path)\n\t}\n\n\tns, n := client.Namespaced(fqn)\n\tcfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tgetter := action.NewGet(cfg)\n\tgetter.Version, err = strconv.Atoi(rev)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp, err := getter.Run(n)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn helm.ReleaseRes{Release: resp}, nil\n}\n\n// Describe returns the chart notes.\nfunc (h *HelmHistory) Describe(path string) (string, error) {\n\trel, err := h.Get(context.Background(), path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tresp, ok := rel.(helm.ReleaseRes)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"expected helm.ReleaseRes, but got %T\", rel)\n\t}\n\n\treturn resp.Release.Info.Notes, nil\n}\n\n// ToYAML returns the chart manifest.\nfunc (h *HelmHistory) ToYAML(path string, _ bool) (string, error) {\n\trel, err := h.Get(context.Background(), path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tresp, ok := rel.(helm.ReleaseRes)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"expected helm.ReleaseRes, but got %T\", rel)\n\t}\n\n\treturn resp.Release.Manifest, nil\n}\n\n// GetValues return the config for this chart.\nfunc (h *HelmHistory) GetValues(path string, allValues bool) ([]byte, error) {\n\trel, err := h.Get(context.Background(), path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tresp, ok := rel.(helm.ReleaseRes)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"expected helm.ReleaseRes, but got %T\", rel)\n\t}\n\n\tvar content any\n\tif allValues {\n\t\tcontent = resp.Release.Chart.Values\n\t} else {\n\t\tcontent = resp.Release.Config\n\t}\n\n\treturn data.WriteYAML(content)\n}\n\nfunc (h *HelmHistory) Rollback(_ context.Context, path, rev string) error {\n\tns, n := client.Namespaced(path)\n\tcfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tver, err := strconv.Atoi(rev)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"could not convert revision to a number: %w\", err)\n\t}\n\tclt := action.NewRollback(cfg)\n\tclt.Version = ver\n\n\treturn clt.Run(n)\n}\n\n// Delete uninstall a Helm.\nfunc (h *HelmHistory) Delete(_ context.Context, path string, _ *metav1.DeletionPropagation, _ Grace) error {\n\tns, n := client.Namespaced(path)\n\tcfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tres, err := action.NewUninstall(cfg).Run(n)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif res != nil && res.Info != \"\" {\n\t\treturn fmt.Errorf(\"%s\", res.Info)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/dao/helpers.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"math\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/cli-runtime/pkg/printers\"\n)\n\nconst (\n\tdefaultServiceAccount = \"default\"\n\n\t// DefaultContainerAnnotation represents the annotation key for the default container.\n\tDefaultContainerAnnotation = \"kubectl.kubernetes.io/default-container\"\n)\n\n// GetDefaultContainer returns a container name if specified in an annotation.\nfunc GetDefaultContainer(m *metav1.ObjectMeta, spec *v1.PodSpec) (string, bool) {\n\tdefaultContainer, ok := m.Annotations[DefaultContainerAnnotation]\n\tif !ok {\n\t\treturn \"\", false\n\t}\n\n\tfor i := range spec.Containers {\n\t\tif spec.Containers[i].Name == defaultContainer {\n\t\t\treturn defaultContainer, true\n\t\t}\n\t}\n\tslog.Warn(\"Container not found. Annotation ignored\",\n\t\tslogs.Container, defaultContainer,\n\t\tslogs.Annotation, DefaultContainerAnnotation,\n\t)\n\n\treturn \"\", false\n}\n\nfunc extractFQN(o runtime.Object) string {\n\tu, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\tslog.Error(\"Expecting unstructured\", slogs.ResType, fmt.Sprintf(\"%T\", o))\n\t\treturn client.NA\n\t}\n\n\treturn FQN(u.GetNamespace(), u.GetName())\n}\n\n// FQN returns a fully qualified resource name.\nfunc FQN(ns, n string) string {\n\tif ns == \"\" {\n\t\treturn n\n\t}\n\treturn ns + \"/\" + n\n}\n\nfunc inList(ll []string, s string) bool {\n\tfor _, l := range ll {\n\t\tif l == s {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc toPerc(v, dv float64) float64 {\n\tif dv == 0 {\n\t\treturn 0\n\t}\n\n\treturn math.Round((v / dv) * 100)\n}\n\n// ToYAML converts a resource to its YAML representation.\nfunc ToYAML(o runtime.Object, showManaged bool) (string, error) {\n\tif o == nil {\n\t\treturn \"\", errors.New(\"no object to yamlize\")\n\t}\n\n\tvar p printers.ResourcePrinter = &printers.YAMLPrinter{}\n\n\tif !showManaged {\n\t\to = o.DeepCopyObject()\n\t\tp = &printers.OmitManagedFieldsPrinter{Delegate: p}\n\t}\n\n\tvar buff bytes.Buffer\n\tif err := p.PrintObj(o, &buff); err != nil {\n\t\tslog.Error(\"PrintObj failed\", slogs.Error, err)\n\t\treturn \"\", err\n\t}\n\n\treturn buff.String(), nil\n}\n\n// serviceAccountMatches validates that the ServiceAccount referenced in the PodSpec matches the incoming\n// ServiceAccount. If the PodSpec ServiceAccount is blank kubernetes will use the \"default\" ServiceAccount\n// when deploying the pod, so if the incoming SA is \"default\" and podSA is an empty string that is also a match.\nfunc serviceAccountMatches(podSA, saName string) bool {\n\tif podSA == \"\" {\n\t\tpodSA = defaultServiceAccount\n\t}\n\n\treturn podSA == saName\n}\n\n// ContinuousRanges takes a sorted slice of integers and returns a slice of\n// sub-slices representing continuous ranges of integers.\nfunc ContinuousRanges(indexes []int) [][]int {\n\tvar ranges [][]int\n\tfor i, p := 1, 0; i <= len(indexes); i++ {\n\t\tif i == len(indexes) || indexes[i]-indexes[p] != i-p {\n\t\t\tranges = append(ranges, []int{indexes[p], indexes[i-1] + 1})\n\t\t\tp = i\n\t\t}\n\t}\n\n\treturn ranges\n}\n"
  },
  {
    "path": "internal/dao/helpers_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\tv1 \"k8s.io/api/core/v1\"\n)\n\nfunc TestToPerc(t *testing.T) {\n\tuu := []struct {\n\t\tv1, v2, e float64\n\t}{\n\t\t{0, 0, 0},\n\t\t{100, 200, 50},\n\t\t{200, 100, 200},\n\t}\n\n\tfor _, u := range uu {\n\t\t//nolint:testifylint\n\t\tassert.Equal(t, u.e, toPerc(u.v1, u.v2))\n\t}\n}\n\nfunc TestServiceAccountMatches(t *testing.T) {\n\tuu := []struct {\n\t\tpodTemplate *v1.PodSpec\n\t\tsaName      string\n\t\texpect      bool\n\t}{\n\t\t{podTemplate: &v1.PodSpec{\n\t\t\tServiceAccountName: \"\",\n\t\t},\n\t\t\tsaName: defaultServiceAccount,\n\t\t\texpect: true,\n\t\t},\n\t\t{podTemplate: &v1.PodSpec{\n\t\t\tServiceAccountName: \"\",\n\t\t},\n\t\t\tsaName: \"foo\",\n\t\t\texpect: false,\n\t\t},\n\t\t{podTemplate: &v1.PodSpec{\n\t\t\tServiceAccountName: \"foo\",\n\t\t},\n\t\t\tsaName: \"foo\",\n\t\t\texpect: true,\n\t\t},\n\t\t{podTemplate: &v1.PodSpec{\n\t\t\tServiceAccountName: \"foo\",\n\t\t},\n\t\t\tsaName: \"bar\",\n\t\t\texpect: false,\n\t\t},\n\t}\n\n\tfor _, u := range uu {\n\t\tassert.Equal(t, u.expect, serviceAccountMatches(u.podTemplate.ServiceAccountName, u.saName))\n\t}\n}\n\nfunc TestContinuousRanges(t *testing.T) {\n\ttests := []struct {\n\t\tIndexes []int\n\t\tRanges  [][]int\n\t}{\n\t\t{\n\t\t\tIndexes: []int{0},\n\t\t\tRanges:  [][]int{{0, 1}},\n\t\t},\n\t\t{\n\t\t\tIndexes: []int{1},\n\t\t\tRanges:  [][]int{{1, 2}},\n\t\t},\n\t\t{\n\t\t\tIndexes: []int{0, 1, 2},\n\t\t\tRanges:  [][]int{{0, 3}},\n\t\t},\n\t\t{\n\t\t\tIndexes: []int{4, 5, 6},\n\t\t\tRanges:  [][]int{{4, 7}},\n\t\t},\n\t\t{\n\t\t\tIndexes: []int{0, 2, 4, 5, 6},\n\t\t\tRanges:  [][]int{{0, 1}, {2, 3}, {4, 7}},\n\t\t},\n\t}\n\n\tfor _, tt := range tests {\n\t\tassert.Equal(t, tt.Ranges, ContinuousRanges(tt.Indexes))\n\t}\n}\n"
  },
  {
    "path": "internal/dao/img_scan.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/vul\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar _ Accessor = (*ImageScan)(nil)\n\n// ImageScan represents vulnerability scans.\ntype ImageScan struct {\n\tNonResource\n}\n\nfunc (is *ImageScan) listImages(ctx context.Context, gvr *client.GVR, path string) ([]string, error) {\n\tres, err := AccessorFor(is.Factory, gvr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ts, ok := res.(ImageLister)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"resource %s is not image lister: %T\", gvr, res)\n\t}\n\n\treturn s.ListImages(ctx, path)\n}\n\n// List returns a collection of scans.\nfunc (is *ImageScan) List(ctx context.Context, _ string) ([]runtime.Object, error) {\n\tfqn, ok := ctx.Value(internal.KeyPath).(string)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"no context path for %q\", is.gvr)\n\t}\n\tgvr, ok := ctx.Value(internal.KeyGVR).(*client.GVR)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"no context gvr for %q\", is.gvr)\n\t}\n\n\tii, err := is.listImages(ctx, gvr, fqn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tres := make([]runtime.Object, 0, len(ii))\n\tfor _, img := range ii {\n\t\ts, ok := vul.ImgScanner.GetScan(img)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, r := range s.Table.Rows {\n\t\t\tres = append(res, render.ImageScanRes{Image: img, Row: r})\n\t\t}\n\t}\n\n\treturn res, nil\n}\n"
  },
  {
    "path": "internal/dao/job.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\tbatchv1 \"k8s.io/api/batch/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar (\n\t_ Accessor    = (*Job)(nil)\n\t_ Nuker       = (*Job)(nil)\n\t_ Loggable    = (*Job)(nil)\n\t_ ImageLister = (*Deployment)(nil)\n)\n\n// Job represents a K8s job resource.\ntype Job struct {\n\tResource\n}\n\n// ListImages lists container images.\nfunc (j *Job) ListImages(_ context.Context, fqn string) ([]string, error) {\n\tjob, err := j.GetInstance(fqn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn render.ExtractImages(&job.Spec.Template.Spec), nil\n}\n\n// List returns a collection of resources.\nfunc (j *Job) List(ctx context.Context, ns string) ([]runtime.Object, error) {\n\too, err := j.Resource.List(ctx, ns)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tctrl, _ := ctx.Value(internal.KeyPath).(string)\n\t_, n := client.Namespaced(ctrl)\n\n\tll := make([]runtime.Object, 0, 10)\n\tfor _, o := range oo {\n\t\tvar j batchv1.Job\n\t\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &j)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(\"expecting Job resource\")\n\t\t}\n\t\tif n == \"\" {\n\t\t\tll = append(ll, o)\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, r := range j.OwnerReferences {\n\t\t\tif r.Name == n {\n\t\t\t\tll = append(ll, o)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn ll, nil\n}\n\n// TailLogs tail logs for all pods represented by this Job.\nfunc (j *Job) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) {\n\to, err := j.getFactory().Get(j.gvr, opts.Path, true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar job batchv1.Job\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &job)\n\tif err != nil {\n\t\treturn nil, errors.New(\"expecting a job resource\")\n\t}\n\n\tif job.Spec.Selector == nil || len(job.Spec.Selector.MatchLabels) == 0 {\n\t\treturn nil, fmt.Errorf(\"no valid selector found for job: %s\", opts.Path)\n\t}\n\n\treturn podLogs(ctx, job.Spec.Selector.MatchLabels, opts)\n}\n\nfunc (j *Job) GetInstance(fqn string) (*batchv1.Job, error) {\n\to, err := j.getFactory().Get(j.gvr, fqn, true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar job batchv1.Job\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &job)\n\tif err != nil {\n\t\treturn nil, errors.New(\"expecting a job resource\")\n\t}\n\n\treturn &job, nil\n}\n\n// ScanSA scans for serviceaccount refs.\nfunc (j *Job) ScanSA(_ context.Context, fqn string, wait bool) (Refs, error) {\n\tns, n := client.Namespaced(fqn)\n\too, err := j.getFactory().List(j.gvr, ns, wait, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trefs := make(Refs, 0, len(oo))\n\tfor _, o := range oo {\n\t\tvar job batchv1.Job\n\t\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &job)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(\"expecting Job resource\")\n\t\t}\n\t\tif serviceAccountMatches(job.Spec.Template.Spec.ServiceAccountName, n) {\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: j.GVR(),\n\t\t\t\tFQN: client.FQN(job.Namespace, job.Name),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn refs, nil\n}\n\n// Scan scans for resource references.\nfunc (j *Job) Scan(_ context.Context, gvr *client.GVR, fqn string, wait bool) (Refs, error) {\n\tns, n := client.Namespaced(fqn)\n\too, err := j.getFactory().List(j.gvr, ns, wait, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trefs := make(Refs, 0, len(oo))\n\tfor _, o := range oo {\n\t\tvar job batchv1.Job\n\t\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &job)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(\"expecting Job resource\")\n\t\t}\n\t\tswitch gvr {\n\t\tcase client.CmGVR:\n\t\t\tif !hasConfigMap(&job.Spec.Template.Spec, n) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: j.GVR(),\n\t\t\t\tFQN: client.FQN(job.Namespace, job.Name),\n\t\t\t})\n\t\tcase client.SecGVR:\n\t\t\tfound, err := hasSecret(j.Factory, &job.Spec.Template.Spec, job.Namespace, n, wait)\n\t\t\tif err != nil {\n\t\t\t\tslog.Warn(\"Locate secret failed\",\n\t\t\t\t\tslogs.FQN, fqn,\n\t\t\t\t\tslogs.Error, err,\n\t\t\t\t)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !found {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: j.GVR(),\n\t\t\t\tFQN: client.FQN(job.Namespace, job.Name),\n\t\t\t})\n\t\tcase client.PcGVR:\n\t\t\tif !hasPC(&job.Spec.Template.Spec, n) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: j.GVR(),\n\t\t\t\tFQN: client.FQN(job.Namespace, job.Name),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn refs, nil\n}\n"
  },
  {
    "path": "internal/dao/log_item.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"bytes\"\n)\n\n// LogChan represents a channel for logs.\ntype LogChan chan *LogItem\n\nvar ItemEOF = new(LogItem)\n\n// LogItem represents a container log line.\ntype LogItem struct {\n\tPod, Container  string\n\tSingleContainer bool\n\tBytes           []byte\n\tIsError         bool\n}\n\n// NewLogItem returns a new item.\nfunc NewLogItem(bb []byte) *LogItem {\n\treturn &LogItem{\n\t\tBytes: bb,\n\t}\n}\n\n// NewLogItemFromString returns a new item.\nfunc NewLogItemFromString(s string) *LogItem {\n\treturn &LogItem{\n\t\tBytes: []byte(s),\n\t}\n}\n\n// ID returns pod and or container based id.\nfunc (l *LogItem) ID() string {\n\tif l.Pod != \"\" {\n\t\treturn l.Pod\n\t}\n\treturn l.Container\n}\n\n// GetTimestamp fetch log lime timestamp\nfunc (l *LogItem) GetTimestamp() string {\n\tindex := bytes.Index(l.Bytes, []byte{' '})\n\tif index < 0 {\n\t\treturn \"\"\n\t}\n\treturn string(l.Bytes[:index])\n}\n\n// Info returns pod and container information.\nfunc (l *LogItem) Info() string {\n\treturn l.Pod + \"::\" + l.Container\n}\n\n// IsEmpty checks if the entry is empty.\nfunc (l *LogItem) IsEmpty() bool {\n\treturn len(l.Bytes) == 0\n}\n\n// Size returns the size of the item.\nfunc (l *LogItem) Size() int {\n\treturn 100 + len(l.Bytes) + len(l.Pod) + len(l.Container)\n}\n\n// Render returns a log line as string.\nfunc (l *LogItem) Render(paint string, showTime bool, bb *bytes.Buffer) {\n\tindex := bytes.Index(l.Bytes, []byte{' '})\n\tif showTime && index > 0 {\n\t\tbb.WriteString(\"[gray::b]\")\n\t\tbb.Write(l.Bytes[:index])\n\t\tbb.WriteString(\" \")\n\t\tif l := 30 - len(l.Bytes[:index]); l > 0 {\n\t\t\tbb.Write(bytes.Repeat([]byte{' '}, l))\n\t\t}\n\t\tbb.WriteString(\"[-::-]\")\n\t}\n\n\tif l.Pod != \"\" {\n\t\tbb.WriteString(\"[\" + paint + \"::]\" + l.Pod)\n\t}\n\n\tif !l.SingleContainer && l.Container != \"\" {\n\t\tif l.Pod != \"\" {\n\t\t\tbb.WriteString(\" \")\n\t\t}\n\t\tbb.WriteString(\"[\" + paint + \"::b]\" + l.Container + \"[-::-] \")\n\t} else if l.Pod != \"\" {\n\t\tbb.WriteString(\"[-::] \")\n\t}\n\n\tif index > 0 {\n\t\tbb.Write(l.Bytes[index+1:])\n\t} else {\n\t\tbb.Write(l.Bytes)\n\t}\n}\n"
  },
  {
    "path": "internal/dao/log_item_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao_test\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/tview\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestLogItemEmpty(t *testing.T) {\n\tuu := map[string]struct {\n\t\ts string\n\t\te bool\n\t}{\n\t\t\"empty\": {s: \"\", e: true},\n\t\t\"full\":  {s: \"Testing 1,2,3...\"},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\ti := dao.NewLogItemFromString(u.s)\n\t\t\tassert.Equal(t, u.e, i.IsEmpty())\n\t\t})\n\t}\n}\n\nfunc TestLogItemRender(t *testing.T) {\n\tuu := map[string]struct {\n\t\topts dao.LogOptions\n\t\tlog  string\n\t\te    string\n\t}{\n\t\t\"empty\": {\n\t\t\topts: dao.LogOptions{},\n\t\t\tlog:  fmt.Sprintf(\"%s %s\\n\", \"2018-12-14T10:36:43.326972-07:00\", \"Testing 1,2,3...\"),\n\t\t\te:    \"Testing 1,2,3...\\n\",\n\t\t},\n\t\t\"container\": {\n\t\t\topts: dao.LogOptions{\n\t\t\t\tContainer: \"fred\",\n\t\t\t},\n\t\t\tlog: fmt.Sprintf(\"%s %s\\n\", \"2018-12-14T10:36:43.326972-07:00\", \"Testing 1,2,3...\"),\n\t\t\te:   \"[yellow::b]fred[-::-] Testing 1,2,3...\\n\",\n\t\t},\n\t\t\"pod\": {\n\t\t\topts: dao.LogOptions{\n\t\t\t\tPath:            \"blee/fred\",\n\t\t\t\tContainer:       \"blee\",\n\t\t\t\tSingleContainer: true,\n\t\t\t},\n\t\t\tlog: fmt.Sprintf(\"%s %s\\n\", \"2018-12-14T10:36:43.326972-07:00\", \"Testing 1,2,3...\"),\n\t\t\te:   \"[yellow::]fred [yellow::b]blee[-::-] Testing 1,2,3...\\n\",\n\t\t},\n\t\t\"full\": {\n\t\t\topts: dao.LogOptions{\n\t\t\t\tPath:            \"blee/fred\",\n\t\t\t\tContainer:       \"blee\",\n\t\t\t\tSingleContainer: true,\n\t\t\t\tShowTimestamp:   true,\n\t\t\t},\n\t\t\tlog: fmt.Sprintf(\"%s %s\\n\", \"2018-12-14T10:36:43.326972-07:00\", \"Testing 1,2,3...\"),\n\t\t\te:   \"[gray::b]2018-12-14T10:36:43.326972-07:00 [-::-][yellow::]fred [yellow::b]blee[-::-] Testing 1,2,3...\\n\",\n\t\t},\n\t\t\"log-level\": {\n\t\t\topts: dao.LogOptions{\n\t\t\t\tPath:            \"blee/fred\",\n\t\t\t\tContainer:       \"\",\n\t\t\t\tSingleContainer: false,\n\t\t\t\tShowTimestamp:   false,\n\t\t\t},\n\t\t\tlog: fmt.Sprintf(\"%s %s\\n\", \"2018-12-14T10:36:43.326972-07:00\", \"2021-10-28T13:06:37Z [INFO] [blah-blah] Testing 1,2,3...\"),\n\t\t\te:   \"[yellow::]fred[-::] 2021-10-28T13:06:37Z [INFO[] [blah-blah[] Testing 1,2,3...\\n\",\n\t\t},\n\t\t\"escape\": {\n\t\t\topts: dao.LogOptions{\n\t\t\t\tPath:            \"blee/fred\",\n\t\t\t\tContainer:       \"\",\n\t\t\t\tSingleContainer: false,\n\t\t\t\tShowTimestamp:   false,\n\t\t\t},\n\t\t\tlog: fmt.Sprintf(\"%s %s\\n\", \"2018-12-14T10:36:43.326972-07:00\", `{\"foo\":[\"bar\"]} Server listening on: [::]:5000`),\n\t\t\te:   `[yellow::]fred[-::] {\"foo\":[\"bar\"[]} Server listening on: [::[]:5000` + \"\\n\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\ti := dao.NewLogItem([]byte(tview.Escape(u.log)))\n\t\t\t_, n := client.Namespaced(u.opts.Path)\n\t\t\ti.Pod, i.Container = n, u.opts.Container\n\n\t\t\tbb := bytes.NewBuffer(make([]byte, 0, i.Size()))\n\t\t\ti.Render(\"yellow\", u.opts.ShowTimestamp, bb)\n\t\t\tassert.Equal(t, u.e, bb.String())\n\t\t})\n\t}\n}\n\nfunc BenchmarkLogItemRenderTS(b *testing.B) {\n\ts := []byte(fmt.Sprintf(\"%s %s\\n\", \"2018-12-14T10:36:43.326972-07:00\", \"Testing 1,2,3...\"))\n\ti := dao.NewLogItem(s)\n\ti.Pod, i.Container = \"fred\", \"blee\"\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\tfor range b.N {\n\t\tbb := bytes.NewBuffer(make([]byte, 0, i.Size()))\n\t\ti.Render(\"yellow\", true, bb)\n\t}\n}\n\nfunc BenchmarkLogItemRenderNoTS(b *testing.B) {\n\ts := []byte(fmt.Sprintf(\"%s %s\\n\", \"2018-12-14T10:36:43.326972-07:00\", \"Testing 1,2,3...\"))\n\ti := dao.NewLogItem(s)\n\ti.Pod, i.Container = \"fred\", \"blee\"\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\tfor range b.N {\n\t\tbb := bytes.NewBuffer(make([]byte, 0, i.Size()))\n\t\ti.Render(\"yellow\", false, bb)\n\t}\n}\n"
  },
  {
    "path": "internal/dao/log_items.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/sahilm/fuzzy\"\n)\n\ntype podColors map[string]string\n\nvar podPalette = []string{\n\t\"teal\",\n\t\"green\",\n\t\"purple\",\n\t\"lime\",\n\t\"blue\",\n\t\"yellow\",\n\t\"fushia\",\n\t\"aqua\",\n}\n\n// LogItems represents a collection of log items.\ntype LogItems struct {\n\titems     []*LogItem\n\tpodColors podColors\n\tmx        sync.RWMutex\n}\n\n// NewLogItems returns a new instance.\nfunc NewLogItems() *LogItems {\n\treturn &LogItems{\n\t\tpodColors: make(map[string]string),\n\t}\n}\n\n// Items returns the log items.\nfunc (l *LogItems) Items() []*LogItem {\n\tl.mx.RLock()\n\tdefer l.mx.RUnlock()\n\n\treturn l.items\n}\n\n// Len returns the items length.\nfunc (l *LogItems) Len() int {\n\tl.mx.RLock()\n\tdefer l.mx.RUnlock()\n\n\treturn len(l.items)\n}\n\n// Clear removes all items.\nfunc (l *LogItems) Clear() {\n\tl.mx.Lock()\n\tdefer l.mx.Unlock()\n\n\tl.items = l.items[:0]\n\tfor k := range l.podColors {\n\t\tdelete(l.podColors, k)\n\t}\n}\n\n// Shift scrolls the lines by one.\nfunc (l *LogItems) Shift(i *LogItem) {\n\tl.mx.Lock()\n\tdefer l.mx.Unlock()\n\n\tl.items = append(l.items[1:], i)\n}\n\n// Subset return a subset of logitems.\nfunc (l *LogItems) Subset(index int) *LogItems {\n\tl.mx.RLock()\n\tdefer l.mx.RUnlock()\n\n\treturn &LogItems{\n\t\titems:     l.items[index:],\n\t\tpodColors: l.podColors,\n\t}\n}\n\n// Merge merges two logitems list.\nfunc (l *LogItems) Merge(n *LogItems) {\n\tl.mx.Lock()\n\tdefer l.mx.Unlock()\n\n\tl.items = append(l.items, n.items...)\n\tfor k, v := range n.podColors {\n\t\tl.podColors[k] = v\n\t}\n}\n\n// Add augments the items.\nfunc (l *LogItems) Add(ii ...*LogItem) {\n\tl.mx.Lock()\n\tdefer l.mx.Unlock()\n\n\tl.items = append(l.items, ii...)\n}\n\nfunc (l *LogItems) podColorFor(id string) string {\n\tcolor, ok := l.podColors[id]\n\tif ok {\n\t\treturn color\n\t}\n\tvar idx int\n\tfor i, r := range id {\n\t\tidx += i * int(r)\n\t}\n\tl.podColors[id] = podPalette[idx%len(podPalette)]\n\n\treturn l.podColors[id]\n}\n\n// Lines returns a collection of log lines.\nfunc (l *LogItems) Lines(index int, showTime bool, ll [][]byte) {\n\tl.mx.Lock()\n\tdefer l.mx.Unlock()\n\n\tfor i, item := range l.items[index:] {\n\t\tbb := bytes.NewBuffer(make([]byte, 0, item.Size()))\n\t\titem.Render(l.podColorFor(item.ID()), showTime, bb)\n\t\tll[i] = bb.Bytes()\n\t}\n}\n\n// StrLines returns a collection of log lines.\nfunc (l *LogItems) StrLines(index int, showTime bool) []string {\n\tl.mx.Lock()\n\tdefer l.mx.Unlock()\n\n\tll := make([]string, len(l.items[index:]))\n\tfor i, item := range l.items[index:] {\n\t\tbb := bytes.NewBuffer(make([]byte, 0, item.Size()))\n\t\titem.Render(l.podColorFor(item.ID()), showTime, bb)\n\t\tll[i] = bb.String()\n\t}\n\n\treturn ll\n}\n\n// Render returns logs as a collection of strings.\nfunc (l *LogItems) Render(index int, showTime bool, ll [][]byte) {\n\tfor i, item := range l.items[index:] {\n\t\tbb := bytes.NewBuffer(make([]byte, 0, item.Size()))\n\t\titem.Render(l.podColorFor(item.ID()), showTime, bb)\n\t\tll[i] = bb.Bytes()\n\t}\n}\n\n// DumpDebug for debugging.\nfunc (l *LogItems) DumpDebug(m string) {\n\tfmt.Println(m + strings.Repeat(\"-\", 50))\n\tfor i, line := range l.items {\n\t\tfmt.Println(i, string(line.Bytes))\n\t}\n}\n\n// Filter filters out log items based on given filter.\nfunc (l *LogItems) Filter(index int, q string, showTime bool) (matches []int, indices [][]int, err error) {\n\tif q == \"\" {\n\t\treturn\n\t}\n\tif f, ok := internal.IsFuzzySelector(q); ok {\n\t\tmatches, indices = l.fuzzyFilter(index, f, showTime)\n\t\treturn\n\t}\n\tmatches, indices, err = l.filterLogs(index, q, showTime)\n\tif err != nil {\n\t\treturn\n\t}\n\n\treturn matches, indices, nil\n}\n\nfunc (l *LogItems) fuzzyFilter(index int, q string, showTime bool) (matches []int, indices [][]int) {\n\tq = strings.TrimSpace(q)\n\tmatches, indices = make([]int, 0, len(l.items)), make([][]int, 0, len(l.items))\n\tmm := fuzzy.Find(q, l.StrLines(index, showTime))\n\tfor _, m := range mm {\n\t\tmatches = append(matches, m.Index)\n\t\tindices = append(indices, m.MatchedIndexes)\n\t}\n\n\treturn matches, indices\n}\n\nfunc (l *LogItems) filterLogs(index int, q string, showTime bool) (matches []int, indices [][]int, err error) {\n\tvar invert bool\n\tif internal.IsInverseSelector(q) {\n\t\tinvert = true\n\t\tq = q[1:]\n\t}\n\trx, err := regexp.Compile(`(?i)` + q)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\tmatches, indices = make([]int, 0, len(l.items)), make([][]int, 0, len(l.items))\n\tll := make([][]byte, len(l.items[index:]))\n\tl.Lines(index, showTime, ll)\n\tfor i, line := range ll {\n\t\tlocs := rx.FindAllIndex(line, -1)\n\t\tif locs != nil && invert {\n\t\t\tcontinue\n\t\t}\n\t\tif locs == nil && !invert {\n\t\t\tcontinue\n\t\t}\n\t\tmatches = append(matches, i)\n\t\tii := make([]int, 0, 10)\n\t\tfor _, loc := range locs {\n\t\t\tfor j := loc[0]; j < loc[1]; j++ {\n\t\t\t\tii = append(ii, j)\n\t\t\t}\n\t\t}\n\t\tindices = append(indices, ii)\n\t}\n\n\treturn matches, indices, nil\n}\n"
  },
  {
    "path": "internal/dao/log_items_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao_test\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc init() {\n\tslog.SetDefault(slog.New(slog.DiscardHandler))\n}\n\nfunc TestLogItemsFilter(t *testing.T) {\n\tuu := map[string]struct {\n\t\tq       string\n\t\topts    dao.LogOptions\n\t\te       []int\n\t\tindices [][]int\n\t\terr     error\n\t}{\n\t\t\"empty\": {\n\t\t\topts: dao.LogOptions{},\n\t\t},\n\t\t\"pod-name\": {\n\t\t\tq: \"blee\",\n\t\t\topts: dao.LogOptions{\n\t\t\t\tPath:      \"fred/blee\",\n\t\t\t\tContainer: \"c1\",\n\t\t\t},\n\t\t\te: []int{0, 1, 2},\n\t\t},\n\t\t\"container-name\": {\n\t\t\tq: \"c1\",\n\t\t\topts: dao.LogOptions{\n\t\t\t\tPath:      \"fred/blee\",\n\t\t\t\tContainer: \"c1\",\n\t\t\t},\n\t\t\te:       []int{0, 1, 2},\n\t\t\tindices: [][]int{{26, 27}, {26, 27}, {26, 27}}, // matches container name \"c1\" at positions 26-27 in rendered format each line\n\t\t},\n\t\t\"message\": {\n\t\t\tq: \"zorg\",\n\t\t\topts: dao.LogOptions{\n\t\t\t\tPath:      \"fred/blee\",\n\t\t\t\tContainer: \"c1\",\n\t\t\t},\n\t\t\te: []int{2},\n\t\t},\n\t\t\"fuzzy\": {\n\t\t\tq: \"-f zorg\",\n\t\t\topts: dao.LogOptions{\n\t\t\t\tPath:      \"fred/blee\",\n\t\t\t\tContainer: \"c1\",\n\t\t\t},\n\t\t\te: []int{2},\n\t\t},\n\t\t\"multi-origin-text-match\": {\n\t\t\tq: \"will\",\n\t\t\topts: dao.LogOptions{\n\t\t\t\tPath:      \"fred/blee\",\n\t\t\t\tContainer: \"c1\",\n\t\t\t},\n\t\t\te:       []int{1, 2},\n\t\t\tindices: [][]int{{45, 46, 47, 48, 59, 60, 61, 62}, {64, 65, 66, 67, 70, 71, 72, 73, 76, 77, 78, 79}},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tii := dao.NewLogItems()\n\t\tii.Add(\n\t\t\tdao.NewLogItem([]byte(fmt.Sprintf(\"%s %s\\n\", \"2018-12-14T10:36:43.326972-07:00\", \"Testing 1,2,3...\"))),\n\t\t\tdao.NewLogItemFromString(\"Bumble bee tuna. will be back. will win.\"),\n\t\t\tdao.NewLogItemFromString(\"Jean Batiste Emmanuel Zorg. wili, will. will, will\"),\n\t\t)\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\t_, n := client.Namespaced(u.opts.Path)\n\t\t\tfor _, i := range ii.Items() {\n\t\t\t\ti.Pod, i.Container = n, u.opts.Container\n\t\t\t}\n\t\t\tres, indices, err := ii.Filter(0, u.q, false)\n\t\t\tassert.Equal(t, u.err, err)\n\t\t\tif err == nil {\n\t\t\t\tassert.Equal(t, u.e, res)\n\t\t\t\tif u.indices != nil {\n\t\t\t\t\tassert.Equal(t, u.indices, indices)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLogItemsRender(t *testing.T) {\n\tuu := map[string]struct {\n\t\topts dao.LogOptions\n\t\te    string\n\t}{\n\t\t\"empty\": {\n\t\t\topts: dao.LogOptions{},\n\t\t\te:    \"Testing 1,2,3...\\n\",\n\t\t},\n\t\t\"container\": {\n\t\t\topts: dao.LogOptions{\n\t\t\t\tContainer: \"fred\",\n\t\t\t},\n\t\t\te: \"[teal::b]fred[-::-] Testing 1,2,3...\\n\",\n\t\t},\n\t\t\"pod-container\": {\n\t\t\topts: dao.LogOptions{\n\t\t\t\tPath:      \"blee/fred\",\n\t\t\t\tContainer: \"blee\",\n\t\t\t},\n\t\t\te: \"[teal::]fred [teal::b]blee[-::-] Testing 1,2,3...\\n\",\n\t\t},\n\t\t\"full\": {\n\t\t\topts: dao.LogOptions{\n\t\t\t\tPath:          \"blee/fred\",\n\t\t\t\tContainer:     \"blee\",\n\t\t\t\tShowTimestamp: true,\n\t\t\t},\n\t\t\te: \"[gray::b]2018-12-14T10:36:43.326972-07:00 [-::-][teal::]fred [teal::b]blee[-::-] Testing 1,2,3...\\n\",\n\t\t},\n\t}\n\n\ts := []byte(fmt.Sprintf(\"%s %s\\n\", \"2018-12-14T10:36:43.326972-07:00\", \"Testing 1,2,3...\"))\n\tfor k := range uu {\n\t\tii := dao.NewLogItems()\n\t\tii.Add(dao.NewLogItem(s))\n\t\tu := uu[k]\n\t\t_, n := client.Namespaced(u.opts.Path)\n\t\tii.Items()[0].Pod, ii.Items()[0].Container = n, u.opts.Container\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tres := make([][]byte, 1)\n\t\t\tii.Render(0, u.opts.ShowTimestamp, res)\n\t\t\tassert.Equal(t, u.e, string(res[0]))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dao/log_options.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// LogOptions represents logger options.\ntype LogOptions struct {\n\tCreateDuration   time.Duration\n\tPath             string\n\tContainer        string\n\tDefaultContainer string\n\tSinceTime        string\n\tLines            int64\n\tSinceSeconds     int64\n\tHead             bool\n\tPrevious         bool\n\tSingleContainer  bool\n\tMultiPods        bool\n\tShowTimestamp    bool\n\tAllContainers    bool\n}\n\n// Info returns the option pod and container info.\nfunc (o *LogOptions) Info() string {\n\tif o.Container != \"\" {\n\t\treturn fmt.Sprintf(\"%s (%s)\", o.Path, o.Container)\n\t}\n\n\treturn o.Path\n}\n\n// Clone clones options.\nfunc (o *LogOptions) Clone() *LogOptions {\n\treturn &LogOptions{\n\t\tPath:             o.Path,\n\t\tContainer:        o.Container,\n\t\tDefaultContainer: o.DefaultContainer,\n\t\tLines:            o.Lines,\n\t\tPrevious:         o.Previous,\n\t\tHead:             o.Head,\n\t\tSingleContainer:  o.SingleContainer,\n\t\tMultiPods:        o.MultiPods,\n\t\tShowTimestamp:    o.ShowTimestamp,\n\t\tSinceTime:        o.SinceTime,\n\t\tSinceSeconds:     o.SinceSeconds,\n\t\tAllContainers:    o.AllContainers,\n\t}\n}\n\n// HasContainer checks if a container is present.\nfunc (o *LogOptions) HasContainer() bool {\n\treturn o.Container != \"\"\n}\n\n// ToggleAllContainers toggles single or all-containers if possible.\nfunc (o *LogOptions) ToggleAllContainers() {\n\tif o.SingleContainer {\n\t\treturn\n\t}\n\to.AllContainers = !o.AllContainers\n\tif o.AllContainers {\n\t\to.DefaultContainer, o.Container = o.Container, \"\"\n\t\treturn\n\t}\n\n\tif o.DefaultContainer != \"\" {\n\t\to.Container = o.DefaultContainer\n\t}\n}\n\n// ToPodLogOptions returns pod log options.\nfunc (o *LogOptions) ToPodLogOptions() *v1.PodLogOptions {\n\topts := v1.PodLogOptions{\n\t\tFollow:     true,\n\t\tTimestamps: true,\n\t\tContainer:  o.Container,\n\t\tPrevious:   o.Previous,\n\t\tTailLines:  &o.Lines,\n\t}\n\tif o.Head {\n\t\tvar maxBytes int64 = 5000\n\t\topts.Follow = false\n\t\topts.TailLines, opts.SinceSeconds, opts.SinceTime = nil, nil, nil\n\t\topts.LimitBytes = &maxBytes\n\t\treturn &opts\n\t}\n\tif o.SinceSeconds < 0 {\n\t\treturn &opts\n\t}\n\n\tif o.SinceSeconds != 0 {\n\t\topts.SinceSeconds, opts.SinceTime = &o.SinceSeconds, nil\n\t\treturn &opts\n\t}\n\n\tif o.SinceTime == \"\" {\n\t\treturn &opts\n\t}\n\tif t, err := time.Parse(time.RFC3339, o.SinceTime); err == nil {\n\t\topts.SinceTime = &metav1.Time{Time: t.Add(time.Second)}\n\t}\n\n\treturn &opts\n}\n\n// ToLogItem add a log header to display po/co information along with the log message.\nfunc (o *LogOptions) ToLogItem(bytes []byte) *LogItem {\n\titem := NewLogItem(bytes)\n\tif len(bytes) == 0 {\n\t\treturn item\n\t}\n\titem.SingleContainer = o.SingleContainer\n\tif item.SingleContainer {\n\t\titem.Container = o.Container\n\t}\n\tif o.MultiPods {\n\t\t_, pod := client.Namespaced(o.Path)\n\t\titem.Pod, item.Container = pod, o.Container\n\t} else {\n\t\titem.Container = o.Container\n\t}\n\n\treturn item\n}\n\nfunc (*LogOptions) ToErrLogItem(err error) *LogItem {\n\tt := time.Now().UTC().Format(time.RFC3339Nano)\n\titem := NewLogItem([]byte(fmt.Sprintf(\"%s [orange::b]%s[::-]\\n\", t, err)))\n\titem.IsError = true\n\treturn item\n}\n"
  },
  {
    "path": "internal/dao/log_options_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestLogOptionsToggleAllContainers(t *testing.T) {\n\tuu := map[string]struct {\n\t\topts dao.LogOptions\n\t\tco   string\n\t\twant bool\n\t}{\n\t\t\"empty\": {\n\t\t\topts: dao.LogOptions{},\n\t\t\twant: true,\n\t\t},\n\t\t\"container\": {\n\t\t\topts: dao.LogOptions{Container: \"blee\"},\n\t\t\twant: true,\n\t\t},\n\t\t\"default-container\": {\n\t\t\topts: dao.LogOptions{AllContainers: true},\n\t\t\tco:   \"blee\",\n\t\t},\n\t\t\"single-container\": {\n\t\t\topts: dao.LogOptions{Container: \"blee\", SingleContainer: true},\n\t\t\tco:   \"blee\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tu.opts.DefaultContainer = \"blee\"\n\t\t\tu.opts.ToggleAllContainers()\n\t\t\tassert.Equal(t, u.want, u.opts.AllContainers)\n\t\t\tassert.Equal(t, u.co, u.opts.Container)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dao/node.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/client-go/kubernetes\"\n\t\"k8s.io/kubectl/pkg/drain\"\n\t\"k8s.io/kubectl/pkg/scheme\"\n\tmv1beta1 \"k8s.io/metrics/pkg/apis/metrics/v1beta1\"\n)\n\nvar (\n\t_ Accessor       = (*Node)(nil)\n\t_ NodeMaintainer = (*Node)(nil)\n)\n\n// NodeMetricsFunc retrieves node metrics.\ntype NodeMetricsFunc func() (*mv1beta1.NodeMetricsList, error)\n\n// Node represents a node model.\ntype Node struct {\n\tResource\n}\n\n// ToggleCordon toggles cordon/uncordon a node.\nfunc (n *Node) ToggleCordon(fqn string, cordon bool) error {\n\tslog.Debug(\"Toggle cordon on node\",\n\t\tslogs.GVR, n.GVR(),\n\t\tslogs.FQN, fqn,\n\t\tslogs.Bool, cordon,\n\t)\n\to, err := FetchNode(context.Background(), n.Factory, fqn)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\th, err := drain.NewCordonHelperFromRuntimeObject(o, scheme.Scheme, n.gvr.GVK())\n\tif err != nil {\n\t\tslog.Debug(\"Fail to toggle cordon on node\",\n\t\t\tslogs.FQN, fqn,\n\t\t\tslogs.Error, err,\n\t\t)\n\t\treturn err\n\t}\n\n\tif !h.UpdateIfRequired(cordon) {\n\t\tif cordon {\n\t\t\treturn fmt.Errorf(\"node is already cordoned\")\n\t\t}\n\t\treturn fmt.Errorf(\"node is already uncordoned\")\n\t}\n\tdial, err := n.getFactory().Client().Dial()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr, patchErr := h.PatchOrReplace(dial, false)\n\tif patchErr != nil {\n\t\treturn patchErr\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (o DrainOptions) toDrainHelper(k kubernetes.Interface, w io.Writer) drain.Helper {\n\treturn drain.Helper{\n\t\tClient:              k,\n\t\tGracePeriodSeconds:  o.GracePeriodSeconds,\n\t\tTimeout:             o.Timeout,\n\t\tDeleteEmptyDirData:  o.DeleteEmptyDirData,\n\t\tIgnoreAllDaemonSets: o.IgnoreAllDaemonSets,\n\t\tDisableEviction:     o.DisableEviction,\n\t\tOut:                 w,\n\t\tErrOut:              w,\n\t\tForce:               o.Force,\n\t}\n}\n\n// Drain drains a node.\nfunc (n *Node) Drain(path string, opts DrainOptions, w io.Writer) error {\n\tcordoned, err := n.ensureCordoned(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !cordoned {\n\t\tif e := n.ToggleCordon(path, true); e != nil {\n\t\t\treturn e\n\t\t}\n\t}\n\n\tdial, err := n.getFactory().Client().Dial()\n\tif err != nil {\n\t\treturn err\n\t}\n\th := opts.toDrainHelper(dial, w)\n\tdd, errs := h.GetPodsForDeletion(path)\n\tif len(errs) != 0 {\n\t\tfor _, e := range errs {\n\t\t\tif _, err := fmt.Fprintf(h.ErrOut, \"[%s] %s\\n\", path, e.Error()); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn errors.Join(errs...)\n\t}\n\n\tif err := h.DeleteOrEvictPods(dd.Pods()); err != nil {\n\t\treturn err\n\t}\n\t_, _ = fmt.Fprintf(h.Out, \"Node %s drained!\", path)\n\n\treturn nil\n}\n\n// Get returns a node resource.\nfunc (n *Node) Get(ctx context.Context, path string) (runtime.Object, error) {\n\too, err := n.Resource.List(ctx, \"\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar raw *unstructured.Unstructured\n\tfor _, o := range oo {\n\t\tif u, ok := o.(*unstructured.Unstructured); ok && u.GetName() == path {\n\t\t\traw = u\n\t\t}\n\t}\n\tif raw == nil {\n\t\treturn nil, fmt.Errorf(\"unable to locate node %s\", path)\n\t}\n\n\tvar nmx *mv1beta1.NodeMetrics\n\tif withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); ok && withMx {\n\t\tnmx, _ = client.DialMetrics(n.Client()).FetchNodeMetrics(ctx, path)\n\t}\n\n\treturn &render.NodeWithMetrics{Raw: raw, MX: nmx}, nil\n}\n\n// List returns a collection of node resources.\nfunc (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) {\n\too, err := n.Resource.List(ctx, ns)\n\tif err != nil {\n\t\treturn oo, err\n\t}\n\n\tvar nmx client.NodesMetricsMap\n\tif withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok {\n\t\tnmx, _ = client.DialMetrics(n.Client()).FetchNodesMetricsMap(ctx)\n\t}\n\n\tshouldCountPods, _ := ctx.Value(internal.KeyPodCounting).(bool)\n\tvar pods []runtime.Object\n\tif shouldCountPods {\n\t\tpods, err = n.getFactory().List(client.PodGVR, client.BlankNamespace, false, labels.Everything())\n\t\tif err != nil {\n\t\t\tslog.Error(\"Unable to list pods\", slogs.Error, err)\n\t\t}\n\t}\n\tres := make([]runtime.Object, 0, len(oo))\n\tfor _, o := range oo {\n\t\tu, ok := o.(*unstructured.Unstructured)\n\t\tif !ok {\n\t\t\treturn res, fmt.Errorf(\"expecting *unstructured.Unstructured but got `%T\", o)\n\t\t}\n\n\t\tfqn := extractFQN(o)\n\t\t_, name := client.Namespaced(fqn)\n\t\tpodCount := -1\n\t\tif shouldCountPods {\n\t\t\tpodCount, err = n.CountPods(pods, name)\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(\"Unable to get pods count\",\n\t\t\t\t\tslogs.ResName, name,\n\t\t\t\t\tslogs.Error, err,\n\t\t\t\t)\n\t\t\t}\n\t\t}\n\t\tres = append(res, &render.NodeWithMetrics{\n\t\t\tRaw:      u,\n\t\t\tMX:       nmx[name],\n\t\t\tPodCount: podCount,\n\t\t})\n\t}\n\n\treturn res, nil\n}\n\n// CountPods counts the pods scheduled on a given node.\nfunc (*Node) CountPods(oo []runtime.Object, nodeName string) (int, error) {\n\tvar count int\n\tfor _, o := range oo {\n\t\tu, ok := o.(*unstructured.Unstructured)\n\t\tif !ok {\n\t\t\treturn count, fmt.Errorf(\"expecting *Unstructured but got `%T\", o)\n\t\t}\n\t\tspec, ok := u.Object[\"spec\"].(map[string]any)\n\t\tif !ok {\n\t\t\treturn count, fmt.Errorf(\"expecting spec interface map but got `%T\", o)\n\t\t}\n\t\tif node, ok := spec[\"nodeName\"]; ok && node == nodeName {\n\t\t\tcount++\n\t\t}\n\t}\n\n\treturn count, nil\n}\n\n// GetPods returns all pods running on given node.\nfunc (n *Node) GetPods(nodeName string) ([]*v1.Pod, error) {\n\too, err := n.getFactory().List(client.PodGVR, client.BlankNamespace, false, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpp := make([]*v1.Pod, 0, len(oo))\n\tfor _, o := range oo {\n\t\tpo := new(v1.Pod)\n\t\tif err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, po); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif po.Spec.NodeName == nodeName {\n\t\t\tpp = append(pp, po)\n\t\t}\n\t}\n\n\treturn pp, nil\n}\n\n// ensureCordoned returns whether the given node has been cordoned\nfunc (n *Node) ensureCordoned(path string) (bool, error) {\n\to, err := FetchNode(context.Background(), n.Factory, path)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\treturn o.Spec.Unschedulable, nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\n// FetchNode retrieves a node.\nfunc FetchNode(_ context.Context, f Factory, path string) (*v1.Node, error) {\n\t_, n := client.Namespaced(path)\n\tauth, err := f.Client().CanI(client.ClusterScope, client.NodeGVR, n, client.GetAccess)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !auth {\n\t\treturn nil, fmt.Errorf(\"user is not authorized to list nodes\")\n\t}\n\n\to, err := f.Get(client.NodeGVR, client.FQN(client.ClusterScope, path), true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar node v1.Node\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &node)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &node, nil\n}\n\n// FetchNodes retrieves all nodes.\nfunc FetchNodes(_ context.Context, f Factory, _ string) (*v1.NodeList, error) {\n\tauth, err := f.Client().CanI(client.ClusterScope, client.NodeGVR, \"\", client.ListAccess)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !auth {\n\t\treturn nil, fmt.Errorf(\"user is not authorized to list nodes\")\n\t}\n\n\too, err := f.List(client.NodeGVR, \"\", false, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tnn := make([]v1.Node, 0, len(oo))\n\tfor _, o := range oo {\n\t\tvar node v1.Node\n\t\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &node)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tnn = append(nn, node)\n\t}\n\n\treturn &v1.NodeList{Items: nn}, nil\n}\n"
  },
  {
    "path": "internal/dao/non_resource.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\n// NonResource represents a non k8s resource.\ntype NonResource struct {\n\tFactory\n\n\tgvr        *client.GVR\n\tmx         sync.RWMutex\n\tincludeObj bool\n}\n\n// Init initializes the resource.\nfunc (n *NonResource) Init(f Factory, gvr *client.GVR) {\n\tn.mx.Lock()\n\tn.Factory, n.gvr = f, gvr\n\tn.mx.Unlock()\n}\n\n// SetIncludeObject sets if resource object should be included in the api server response.\nfunc (n *NonResource) SetIncludeObject(f bool) {\n\tn.includeObj = f\n}\n\nfunc (n *NonResource) gvrStr() string {\n\tn.mx.RLock()\n\tdefer n.mx.RUnlock()\n\n\treturn n.gvr.String()\n}\n\nfunc (n *NonResource) getFactory() Factory {\n\tn.mx.RLock()\n\tdefer n.mx.RUnlock()\n\n\treturn n.Factory\n}\n\n// GVR returns a gvr.\nfunc (n *NonResource) GVR() string {\n\tn.mx.RLock()\n\tdefer n.mx.RUnlock()\n\n\treturn n.gvrStr()\n}\n\n// Get returns the given resource.\nfunc (*NonResource) Get(context.Context, string) (runtime.Object, error) {\n\treturn nil, fmt.Errorf(\"nyi\")\n}\n"
  },
  {
    "path": "internal/dao/ns.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nvar _ Accessor = (*Namespace)(nil)\n\n// Namespace represents a namespace resource.\ntype Namespace struct {\n\tResource\n}\n"
  },
  {
    "path": "internal/dao/patch.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"encoding/json\"\n)\n\n// ImageSpec represents a container image.\ntype ImageSpec struct {\n\tIndex             int\n\tName, DockerImage string\n\tInit              bool\n}\n\n// ImageSpecs represents a collection of container images.\ntype ImageSpecs []ImageSpec\n\n// JsonPatch track pod spec updates.\ntype JsonPatch struct {\n\tSpec Spec `json:\"spec\"`\n}\n\n// Spec represents a pod template.\ntype Spec struct {\n\tTemplate PodSpec `json:\"template\"`\n}\n\n// PodSpec represents a collection of container images.\ntype PodSpec struct {\n\tSpec ImagesSpec `json:\"spec\"`\n}\n\n// ImagesSpec tracks container image updates.\ntype ImagesSpec struct {\n\tSetElementOrderContainers     []Element `json:\"$setElementOrder/containers,omitempty\"`\n\tSetElementOrderInitContainers []Element `json:\"$setElementOrder/initContainers,omitempty\"`\n\tContainers                    []Element `json:\"containers,omitempty\"`\n\tInitContainers                []Element `json:\"initContainers,omitempty\"`\n}\n\n// Element tracks a given container image.\ntype Element struct {\n\tImage string `json:\"image,omitempty\"`\n\tName  string `json:\"name\"`\n}\n\n// GetTemplateJsonPatch builds a json patch string to update PodSpec images.\nfunc GetTemplateJsonPatch(imageSpecs ImageSpecs) ([]byte, error) {\n\tjsonPatch := JsonPatch{\n\t\tSpec: Spec{\n\t\t\tTemplate: getPatchPodSpec(imageSpecs),\n\t\t},\n\t}\n\treturn json.Marshal(jsonPatch)\n}\n\n// GetJsonPatch returns container image patch.\nfunc GetJsonPatch(imageSpecs ImageSpecs) ([]byte, error) {\n\tpodSpec := getPatchPodSpec(imageSpecs)\n\treturn json.Marshal(podSpec)\n}\n\nfunc getPatchPodSpec(imageSpecs ImageSpecs) PodSpec {\n\tinitElementsOrders, initElements, elementsOrders, elements := extractElements(imageSpecs)\n\tpodSpec := PodSpec{\n\t\tSpec: ImagesSpec{\n\t\t\tSetElementOrderInitContainers: initElementsOrders,\n\t\t\tInitContainers:                initElements,\n\t\t\tSetElementOrderContainers:     elementsOrders,\n\t\t\tContainers:                    elements,\n\t\t},\n\t}\n\treturn podSpec\n}\n\nfunc extractElements(imageSpecs ImageSpecs) (initElementsOrders, initElements, elementsOrders, elements []Element) {\n\tfor _, spec := range imageSpecs {\n\t\tif spec.Init {\n\t\t\tinitElementsOrders = append(initElementsOrders, Element{Name: spec.Name})\n\t\t\tinitElements = append(initElements, Element{Name: spec.Name, Image: spec.DockerImage})\n\t\t} else {\n\t\t\telementsOrders = append(elementsOrders, Element{Name: spec.Name})\n\t\t\telements = append(elements, Element{Name: spec.Name, Image: spec.DockerImage})\n\t\t}\n\t}\n\treturn initElementsOrders, initElements, elementsOrders, elements\n}\n"
  },
  {
    "path": "internal/dao/patch_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetTemplateJsonPatch(t *testing.T) {\n\ttype args struct {\n\t\timageSpecs ImageSpecs\n\t}\n\tuu := map[string]struct {\n\t\targs    args\n\t\twant    string\n\t\twantErr bool\n\t}{\n\t\t\"simple\": {\n\t\t\targs: args{\n\t\t\t\timageSpecs: ImageSpecs{\n\t\t\t\t\tImageSpec{\n\t\t\t\t\t\tIndex:       0,\n\t\t\t\t\t\tName:        \"init\",\n\t\t\t\t\t\tDockerImage: \"busybox:latest\",\n\t\t\t\t\t\tInit:        true,\n\t\t\t\t\t},\n\t\t\t\t\tImageSpec{\n\t\t\t\t\t\tIndex:       0,\n\t\t\t\t\t\tName:        \"nginx\",\n\t\t\t\t\t\tDockerImage: \"nginx:latest\",\n\t\t\t\t\t\tInit:        false,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant:    `{\"spec\":{\"template\":{\"spec\":{\"$setElementOrder/initContainers\":[{\"name\":\"init\"}],\"$setElementOrder/containers\":[{\"name\":\"nginx\"}],\"initContainers\":[{\"image\":\"busybox:latest\",\"name\":\"init\"}],\"containers\":[{\"image\":\"nginx:latest\",\"name\":\"nginx\"}]}}}}`,\n\t\t\twantErr: false,\n\t\t},\n\t}\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tgot, err := GetTemplateJsonPatch(u.args.imageSpecs)\n\t\t\tif (err != nil) != u.wantErr {\n\t\t\t\tt.Errorf(\"GetTemplateJsonPatch() error = %v, wantErr %v\", err, u.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.JSONEq(t, u.want, string(got), \"Json strings should be equal\")\n\t\t})\n\t}\n}\n\nfunc TestGetJsonPatch(t *testing.T) {\n\ttype args struct {\n\t\timageSpecs ImageSpecs\n\t}\n\tuu := map[string]struct {\n\t\targs    args\n\t\twant    string\n\t\twantErr bool\n\t}{\n\t\t\"simple\": {\n\t\t\targs: args{\n\t\t\t\timageSpecs: ImageSpecs{\n\t\t\t\t\tImageSpec{\n\t\t\t\t\t\tIndex:       0,\n\t\t\t\t\t\tName:        \"init\",\n\t\t\t\t\t\tDockerImage: \"busybox:latest\",\n\t\t\t\t\t\tInit:        true,\n\t\t\t\t\t},\n\t\t\t\t\tImageSpec{\n\t\t\t\t\t\tIndex:       0,\n\t\t\t\t\t\tName:        \"nginx\",\n\t\t\t\t\t\tDockerImage: \"nginx:latest\",\n\t\t\t\t\t\tInit:        false,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\twant:    `{\"spec\":{\"$setElementOrder/initContainers\":[{\"name\":\"init\"}],\"initContainers\":[{\"image\":\"busybox:latest\",\"name\":\"init\"}],\"$setElementOrder/containers\":[{\"name\":\"nginx\"}],\"containers\":[{\"image\":\"nginx:latest\",\"name\":\"nginx\"}]}}`,\n\t\t\twantErr: false,\n\t\t},\n\t}\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tgot, err := GetJsonPatch(u.args.imageSpecs)\n\t\t\tif (err != nil) != u.wantErr {\n\t\t\t\tt.Errorf(\"GetTemplateJsonPatch() error = %v, wantErr %v\", err, u.wantErr)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.JSONEq(t, u.want, string(got), \"Json strings should be equal\")\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dao/pod.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v4\"\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/watch\"\n\t\"github.com/derailed/tview\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/types\"\n\t\"k8s.io/apimachinery/pkg/util/sets\"\n\trestclient \"k8s.io/client-go/rest\"\n\tmv1beta1 \"k8s.io/metrics/pkg/apis/metrics/v1beta1\"\n)\n\nvar (\n\t_ Accessor        = (*Pod)(nil)\n\t_ Nuker           = (*Pod)(nil)\n\t_ Loggable        = (*Pod)(nil)\n\t_ Controller      = (*Pod)(nil)\n\t_ ContainsPodSpec = (*Pod)(nil)\n\t_ ImageLister     = (*Pod)(nil)\n)\n\ntype streamResult int\n\nconst (\n\tlogRetryCount                  = 20\n\tlogBackoffInitial              = 500 * time.Millisecond\n\tlogBackoffMax                  = 30 * time.Second\n\tlogChannelBuffer               = 50   // Buffer size for log channel to reduce drops\n\tstreamEOF         streamResult = iota // legit container log close (no retry)\n\tstreamError                           // retryable error (network, auth, etc.)\n\tstreamCanceled                        // context canceled\n)\n\n// Pod represents a pod resource.\ntype Pod struct {\n\tResource\n}\n\n// shouldStopRetrying checks if we should stop retrying log streaming based on pod status.\nfunc (p *Pod) shouldStopRetrying(path string) bool {\n\tpod, err := p.GetInstance(path)\n\tif err != nil {\n\t\treturn true\n\t}\n\n\tif pod.DeletionTimestamp != nil {\n\t\treturn true\n\t}\n\n\tswitch pod.Status.Phase {\n\tcase v1.PodSucceeded, v1.PodFailed:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// Get returns a resource instance if found, else an error.\nfunc (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) {\n\to, err := p.Resource.Get(ctx, path)\n\tif err != nil {\n\t\treturn o, err\n\t}\n\n\tu, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"expecting *unstructured.Unstructured but got `%T\", o)\n\t}\n\n\tvar pmx *mv1beta1.PodMetrics\n\tif withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); ok && withMx {\n\t\tpmx, _ = client.DialMetrics(p.Client()).FetchPodMetrics(ctx, path)\n\t}\n\n\treturn &render.PodWithMetrics{Raw: u, MX: pmx}, nil\n}\n\n// ListImages lists container images.\nfunc (p *Pod) ListImages(_ context.Context, path string) ([]string, error) {\n\tpod, err := p.GetInstance(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn render.ExtractImages(&pod.Spec), nil\n}\n\n// List returns a collection of nodes.\nfunc (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) {\n\too, err := p.Resource.List(ctx, ns)\n\tif err != nil {\n\t\treturn oo, err\n\t}\n\n\tvar pmx client.PodsMetricsMap\n\tif withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); ok && withMx {\n\t\tpmx, _ = client.DialMetrics(p.Client()).FetchPodsMetricsMap(ctx, ns)\n\t}\n\tsel, _ := ctx.Value(internal.KeyFields).(string)\n\tfsel, err := labels.ConvertSelectorToLabelsMap(sel)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tnodeName := fsel[\"spec.nodeName\"]\n\n\tres := make([]runtime.Object, 0, len(oo))\n\tfor _, o := range oo {\n\t\tu, ok := o.(*unstructured.Unstructured)\n\t\tif !ok {\n\t\t\treturn res, fmt.Errorf(\"expecting *unstructured.Unstructured but got `%T\", o)\n\t\t}\n\t\tfqn := extractFQN(o)\n\t\tif nodeName == \"\" {\n\t\t\tres = append(res, &render.PodWithMetrics{Raw: u, MX: pmx[fqn]})\n\t\t\tcontinue\n\t\t}\n\n\t\tspec, ok := u.Object[\"spec\"].(map[string]any)\n\t\tif !ok {\n\t\t\treturn res, fmt.Errorf(\"expecting interface map but got `%T\", o)\n\t\t}\n\t\tif spec[\"nodeName\"] == nodeName {\n\t\t\tres = append(res, &render.PodWithMetrics{Raw: u, MX: pmx[fqn]})\n\t\t}\n\t}\n\n\treturn res, nil\n}\n\n// Logs fetch container logs for a given pod and container.\nfunc (p *Pod) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, error) {\n\tns, n := client.Namespaced(path)\n\tauth, err := p.Client().CanI(ns, client.NewGVR(client.PodGVR.String()+\":log\"), n, client.GetAccess)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !auth {\n\t\treturn nil, fmt.Errorf(\"user is not authorized to view pod logs\")\n\t}\n\n\tdial, err := p.Client().DialLogs()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn dial.CoreV1().Pods(ns).GetLogs(n, opts), nil\n}\n\n// Containers returns all container names on pod.\nfunc (p *Pod) Containers(path string, includeInit bool) ([]string, error) {\n\tpod, err := p.GetInstance(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcc := make([]string, 0, len(pod.Spec.Containers)+len(pod.Spec.InitContainers))\n\tfor i := range pod.Spec.Containers {\n\t\tcc = append(cc, pod.Spec.Containers[i].Name)\n\t}\n\n\tif includeInit {\n\t\tfor i := range pod.Spec.InitContainers {\n\t\t\tcc = append(cc, pod.Spec.InitContainers[i].Name)\n\t\t}\n\t}\n\n\treturn cc, nil\n}\n\n// Pod returns a pod victim by name.\nfunc (*Pod) Pod(fqn string) (string, error) {\n\treturn fqn, nil\n}\n\n// GetInstance returns a pod instance.\nfunc (p *Pod) GetInstance(fqn string) (*v1.Pod, error) {\n\to, err := p.getFactory().Get(p.gvr, fqn, true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar pod v1.Pod\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &pod, nil\n}\n\n// TailLogs tails a given container logs.\nfunc (p *Pod) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) {\n\tfac, ok := ctx.Value(internal.KeyFactory).(*watch.Factory)\n\tif !ok {\n\t\treturn nil, errors.New(\"no factory in context\")\n\t}\n\to, err := fac.Get(p.gvr, opts.Path, true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar po v1.Pod\n\tif err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &po); err != nil {\n\t\treturn nil, err\n\t}\n\tcoCounts := len(po.Spec.InitContainers) + len(po.Spec.Containers) + len(po.Spec.EphemeralContainers)\n\tif coCounts == 1 {\n\t\topts.SingleContainer = true\n\t}\n\n\touts := make([]LogChan, 0, coCounts)\n\tif co, ok := GetDefaultContainer(&po.ObjectMeta, &po.Spec); ok && !opts.AllContainers {\n\t\topts.DefaultContainer = co\n\t\treturn append(outs, tailLogs(ctx, p, opts)), nil\n\t}\n\tif opts.HasContainer() && !opts.AllContainers {\n\t\treturn append(outs, tailLogs(ctx, p, opts)), nil\n\t}\n\tfor i := range po.Spec.InitContainers {\n\t\tcfg := opts.Clone()\n\t\tcfg.Container = po.Spec.InitContainers[i].Name\n\t\touts = append(outs, tailLogs(ctx, p, cfg))\n\t}\n\tfor i := range po.Spec.Containers {\n\t\tcfg := opts.Clone()\n\t\tcfg.Container = po.Spec.Containers[i].Name\n\t\touts = append(outs, tailLogs(ctx, p, cfg))\n\t}\n\tfor i := range po.Spec.EphemeralContainers {\n\t\tcfg := opts.Clone()\n\t\tcfg.Container = po.Spec.EphemeralContainers[i].Name\n\t\touts = append(outs, tailLogs(ctx, p, cfg))\n\t}\n\n\treturn outs, nil\n}\n\n// ScanSA scans for ServiceAccount refs.\nfunc (p *Pod) ScanSA(_ context.Context, fqn string, wait bool) (Refs, error) {\n\tns, n := client.Namespaced(fqn)\n\too, err := p.getFactory().List(p.gvr, ns, wait, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trefs := make(Refs, 0, len(oo))\n\tfor _, o := range oo {\n\t\tvar pod v1.Pod\n\t\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(\"expecting Deployment resource\")\n\t\t}\n\t\t// Just pick controller less pods...\n\t\tif len(pod.OwnerReferences) > 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif serviceAccountMatches(pod.Spec.ServiceAccountName, n) {\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: p.GVR(),\n\t\t\t\tFQN: client.FQN(pod.Namespace, pod.Name),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn refs, nil\n}\n\n// Scan scans for cluster resource refs.\nfunc (p *Pod) Scan(_ context.Context, gvr *client.GVR, fqn string, wait bool) (Refs, error) {\n\tns, n := client.Namespaced(fqn)\n\too, err := p.getFactory().List(p.gvr, ns, wait, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trefs := make(Refs, 0, len(oo))\n\tfor _, o := range oo {\n\t\tvar pod v1.Pod\n\t\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(\"expecting Pod resource\")\n\t\t}\n\t\t// Just pick controller less pods...\n\t\tif len(pod.OwnerReferences) > 0 {\n\t\t\tcontinue\n\t\t}\n\t\tswitch gvr {\n\t\tcase client.CmGVR:\n\t\t\tif !hasConfigMap(&pod.Spec, n) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: p.GVR(),\n\t\t\t\tFQN: client.FQN(pod.Namespace, pod.Name),\n\t\t\t})\n\t\tcase client.SecGVR:\n\t\t\tfound, err := hasSecret(p.Factory, &pod.Spec, pod.Namespace, n, wait)\n\t\t\tif err != nil {\n\t\t\t\tslog.Warn(\"Locate secret failed\",\n\t\t\t\t\tslogs.FQN, fqn,\n\t\t\t\t\tslogs.Error, err,\n\t\t\t\t)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !found {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: p.GVR(),\n\t\t\t\tFQN: client.FQN(pod.Namespace, pod.Name),\n\t\t\t})\n\t\tcase client.PvcGVR:\n\t\t\tif !hasPVC(&pod.Spec, n) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: p.GVR(),\n\t\t\t\tFQN: client.FQN(pod.Namespace, pod.Name),\n\t\t\t})\n\t\tcase client.PcGVR:\n\t\t\tif !hasPC(&pod.Spec, n) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: p.GVR(),\n\t\t\t\tFQN: client.FQN(pod.Namespace, pod.Name),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn refs, nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc tailLogs(ctx context.Context, logger Logger, opts *LogOptions) LogChan {\n\tout := make(LogChan, logChannelBuffer)\n\tvar wg sync.WaitGroup\n\n\twg.Add(1)\n\tgo func() {\n\t\tdefer wg.Done()\n\t\tpodOpts := opts.ToPodLogOptions()\n\n\t\t// Setup exponential backoff following project pattern\n\t\tbf := backoff.NewExponentialBackOff()\n\t\tbf.InitialInterval = logBackoffInitial\n\t\tbf.MaxElapsedTime = 0\n\t\tbf.MaxInterval = logBackoffMax / 2\n\t\tbackoffCtx := backoff.WithContext(bf, ctx)\n\t\tdelay := logBackoffInitial\n\n\t\tfor range logRetryCount {\n\t\t\treq, err := logger.Logs(opts.Path, podOpts)\n\t\t\tif err != nil {\n\t\t\t\tslog.Error(\"Log request failed\",\n\t\t\t\t\tslogs.Container, opts.Info(),\n\t\t\t\t\tslogs.Error, err,\n\t\t\t\t)\n\t\t\t\t// Check if we should stop retrying based on pod status\n\t\t\t\tif pod, ok := logger.(*Pod); ok && pod.shouldStopRetrying(opts.Path) {\n\t\t\t\t\tslog.Debug(\"Stopping log retry - pod is terminating or deleted\",\n\t\t\t\t\t\tslogs.Container, opts.Info(),\n\t\t\t\t\t)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\treturn\n\t\t\t\tcase <-time.After(delay):\n\t\t\t\t\tif delay = backoffCtx.NextBackOff(); delay == backoff.Stop {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tstream, e := req.Stream(ctx)\n\t\t\tif e != nil {\n\t\t\t\tslog.Error(\"Stream logs failed\",\n\t\t\t\t\tslogs.Error, e,\n\t\t\t\t\tslogs.Container, opts.Info(),\n\t\t\t\t)\n\t\t\t\t// Check if we should stop retrying based on pod status\n\t\t\t\tif pod, ok := logger.(*Pod); ok && pod.shouldStopRetrying(opts.Path) {\n\t\t\t\t\tslog.Debug(\"Stopping log retry - pod is terminating or deleted\",\n\t\t\t\t\t\tslogs.Container, opts.Info(),\n\t\t\t\t\t)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\treturn\n\t\t\t\tcase <-time.After(delay):\n\t\t\t\t\tif delay = backoffCtx.NextBackOff(); delay == backoff.Stop {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Process logs until completion\n\t\t\tresult := readLogs(ctx, stream, out, opts)\n\t\t\tswitch result {\n\t\t\tcase streamEOF:\n\t\t\t\tslog.Debug(\"Log stream ended cleanly\",\n\t\t\t\t\tslogs.Container, opts.Info(),\n\t\t\t\t)\n\t\t\t\treturn\n\t\t\tcase streamError:\n\t\t\t\t// Check if we should stop retrying based on pod status\n\t\t\t\tif pod, ok := logger.(*Pod); ok && pod.shouldStopRetrying(opts.Path) {\n\t\t\t\t\tslog.Debug(\"Stopping log retry after stream error - pod is terminating or deleted\",\n\t\t\t\t\t\tslogs.Container, opts.Info(),\n\t\t\t\t\t)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tslog.Debug(\"Log stream error, retrying\",\n\t\t\t\t\tslogs.Container, opts.Info(),\n\t\t\t\t)\n\t\t\t\tselect {\n\t\t\t\tcase <-ctx.Done():\n\t\t\t\t\treturn\n\t\t\t\tcase <-time.After(delay):\n\t\t\t\t\tif delay = backoffCtx.NextBackOff(); delay == backoff.Stop {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\tcase streamCanceled:\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// Reset backoff and delay on successful connection\n\t\t\tbf.Reset()\n\t\t\tdelay = logBackoffInitial\n\t\t}\n\n\t\t// Out of retries\n\t\tout <- opts.ToErrLogItem(fmt.Errorf(\"failed to maintain log stream after %d retries\", logRetryCount))\n\t}()\n\n\tgo func() {\n\t\twg.Wait()\n\t\tclose(out)\n\t}()\n\n\treturn out\n}\n\nfunc readLogs(ctx context.Context, stream io.ReadCloser, out chan<- *LogItem, opts *LogOptions) streamResult {\n\tdefer func() {\n\t\tif err := stream.Close(); err != nil && !errors.Is(err, io.ErrClosedPipe) {\n\t\t\tslog.Error(\"Failed to close stream\",\n\t\t\t\tslogs.Container, opts.Info(),\n\t\t\t\tslogs.Error, err,\n\t\t\t)\n\t\t}\n\t}()\n\n\tr := bufio.NewReader(stream)\n\n\tfor {\n\t\tbytes, err := r.ReadBytes('\\n')\n\t\tif err == nil {\n\t\t\titem := opts.ToLogItem(tview.EscapeBytes(bytes))\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn streamCanceled\n\t\t\tcase out <- item:\n\t\t\tdefault:\n\t\t\t\t// Avoid deadlock if consumer is too slow\n\t\t\t\tslog.Warn(\"Dropping log line due to slow consumer\",\n\t\t\t\t\tslogs.Container, opts.Info(),\n\t\t\t\t)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif errors.Is(err, io.EOF) {\n\t\t\tif len(bytes) > 0 {\n\t\t\t\t// Emit trailing partial line before EOF\n\t\t\t\tout <- opts.ToLogItem(tview.EscapeBytes(bytes))\n\t\t\t}\n\t\t\tslog.Debug(\"Log reader reached EOF\", slogs.Container, opts.Info())\n\t\t\tout <- opts.ToErrLogItem(fmt.Errorf(\"stream closed: %w for %s\", err, opts.Info()))\n\t\t\treturn streamEOF\n\t\t}\n\n\t\t// Non-EOF error\n\t\tslog.Debug(\"Log stream error, will retry connection\",\n\t\t\tslogs.Container, opts.Info(),\n\t\t\tslogs.Error, fmt.Errorf(\"stream error: %w for %s\", err, opts.Info()),\n\t\t)\n\t\t// Don't send stream errors to user - they will be retried\n\t\t// Only final retry exhaustion message is shown\n\t\treturn streamError\n\t}\n}\n\n// MetaFQN returns a fully qualified resource name.\nfunc MetaFQN(m *metav1.ObjectMeta) string {\n\tif m.Namespace == \"\" {\n\t\treturn m.Name\n\t}\n\n\treturn FQN(m.Namespace, m.Name)\n}\n\n// GetPodSpec returns a pod spec given a resource.\nfunc (p *Pod) GetPodSpec(path string) (*v1.PodSpec, error) {\n\tpod, err := p.GetInstance(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpodSpec := pod.Spec\n\n\treturn &podSpec, nil\n}\n\n// SetImages sets container images.\nfunc (p *Pod) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {\n\tns, n := client.Namespaced(path)\n\tauth, err := p.Client().CanI(ns, p.gvr, n, client.PatchAccess)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !auth {\n\t\treturn fmt.Errorf(\"user is not authorized to patch a deployment\")\n\t}\n\tmanager, isManaged, err := p.isControlled(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif isManaged {\n\t\treturn fmt.Errorf(\"unable to set image. This pod is managed by %s. Please set the image on the controller\", manager)\n\t}\n\tjsonPatch, err := GetJsonPatch(imageSpecs)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdial, err := p.Client().Dial()\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = dial.CoreV1().Pods(ns).Patch(\n\t\tctx,\n\t\tn,\n\t\ttypes.StrategicMergePatchType,\n\t\tjsonPatch,\n\t\tmetav1.PatchOptions{},\n\t)\n\n\treturn err\n}\n\nfunc (p *Pod) isControlled(path string) (fqn string, ok bool, err error) {\n\tpod, err := p.GetInstance(path)\n\tif err != nil {\n\t\treturn \"\", false, err\n\t}\n\treferences := pod.GetObjectMeta().GetOwnerReferences()\n\tif len(references) > 0 {\n\t\treturn fmt.Sprintf(\"%s/%s\", references[0].Kind, references[0].Name), true, nil\n\t}\n\n\treturn \"\", false, nil\n}\n\nvar toastPhases = sets.New(\n\trender.PhaseCompleted,\n\trender.PhasePending,\n\trender.PhaseCrashLoop,\n\trender.PhaseError,\n\trender.PhaseImagePullBackOff,\n\trender.PhaseContainerStatusUnknown,\n\trender.PhaseEvicted,\n\trender.PhaseOOMKilled,\n)\n\nfunc (p *Pod) Sanitize(ctx context.Context, ns string) (int, error) {\n\too, err := p.Resource.List(ctx, ns)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tvar count int\n\tfor _, o := range oo {\n\t\tu, ok := o.(*unstructured.Unstructured)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tvar pod v1.Pod\n\t\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &pod)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif toastPhases.Has(render.PodStatus(&pod)) {\n\t\t\t// !!BOZO!! Might need to bump timeout otherwise rev limit if too many??\n\t\t\tfqn := client.FQN(pod.Namespace, pod.Name)\n\t\t\tslog.Debug(\"Sanitizing resource\", slogs.FQN, fqn)\n\t\t\tif err := p.Delete(ctx, fqn, nil, 0); err != nil {\n\t\t\t\tslog.Debug(\"Aborted! Sanitizer delete failed\",\n\t\t\t\t\tslogs.FQN, fqn,\n\t\t\t\t\tslogs.Count, count,\n\t\t\t\t\tslogs.Error, err,\n\t\t\t\t)\n\t\t\t\treturn count, err\n\t\t\t}\n\t\t\tcount++\n\t\t}\n\t}\n\tslog.Debug(\"Sanitizer deleted pods\", slogs.Count, count)\n\n\treturn count, nil\n}\n"
  },
  {
    "path": "internal/dao/pod_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nfunc TestGetDefaultContainer(t *testing.T) {\n\tuu := map[string]struct {\n\t\tpo            *v1.Pod\n\t\twantContainer string\n\t\twantOk        bool\n\t}{\n\t\t\"no_annotation\": {\n\t\t\tpo: &v1.Pod{\n\t\t\t\tSpec: v1.PodSpec{\n\t\t\t\t\tContainers: []v1.Container{{Name: \"container1\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantContainer: \"\",\n\t\t\twantOk:        false,\n\t\t},\n\t\t\"container_not_present\": {\n\t\t\tpo: &v1.Pod{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tAnnotations: map[string]string{DefaultContainerAnnotation: \"container1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantContainer: \"\",\n\t\t\twantOk:        false,\n\t\t},\n\t\t\"container_found\": {\n\t\t\tpo: &v1.Pod{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tAnnotations: map[string]string{DefaultContainerAnnotation: \"container1\"},\n\t\t\t\t},\n\t\t\t\tSpec: v1.PodSpec{\n\t\t\t\t\tContainers: []v1.Container{{Name: \"container1\"}},\n\t\t\t\t},\n\t\t\t},\n\t\t\twantContainer: \"container1\",\n\t\t\twantOk:        true,\n\t\t},\n\t}\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tcontainer, ok := GetDefaultContainer(&u.po.ObjectMeta, &u.po.Spec)\n\t\t\tassert.Equal(t, u.wantContainer, container)\n\t\t\tassert.Equal(t, u.wantOk, ok)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dao/port_forward.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar (\n\t_ Accessor = (*PortForward)(nil)\n\t_ Nuker    = (*PortForward)(nil)\n)\n\n// PortForward represents a port forward dao.\ntype PortForward struct {\n\tNonResource\n}\n\n// Delete deletes a portforward.\nfunc (p *PortForward) Delete(_ context.Context, path string, _ *metav1.DeletionPropagation, _ Grace) error {\n\tp.getFactory().DeleteForwarder(path)\n\n\treturn nil\n}\n\n// List returns a collection of port forwards.\nfunc (p *PortForward) List(ctx context.Context, _ string) ([]runtime.Object, error) {\n\tbenchFile, ok := ctx.Value(internal.KeyBenchCfg).(string)\n\tif !ok || benchFile == \"\" {\n\t\treturn nil, fmt.Errorf(\"no benchmark config file found in context\")\n\t}\n\tpath, _ := ctx.Value(internal.KeyPath).(string)\n\n\tbcfg, err := config.NewBench(benchFile)\n\tif err != nil {\n\t\tslog.Debug(\"No custom benchmark config file found\", slogs.FileName, benchFile)\n\t}\n\n\tff, cc := p.getFactory().Forwarders(), bcfg.Benchmarks.Containers\n\too := make([]runtime.Object, 0, len(ff))\n\tfor k, f := range ff {\n\t\tif !strings.HasPrefix(k, path) {\n\t\t\tcontinue\n\t\t}\n\t\tcfg := render.BenchCfg{\n\t\t\tC: bcfg.Benchmarks.Defaults.C,\n\t\t\tN: bcfg.Benchmarks.Defaults.N,\n\t\t}\n\t\tif cust, ok := cc[PodToKey(k)]; ok {\n\t\t\tcfg.C, cfg.N = cust.C, cust.N\n\t\t\tcfg.Host, cfg.Path = cust.HTTP.Host, cust.HTTP.Path\n\t\t}\n\t\too = append(oo, render.ForwardRes{\n\t\t\tForwarder: f,\n\t\t\tConfig:    cfg,\n\t\t})\n\t}\n\n\treturn oo, nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nvar podNameRX = regexp.MustCompile(`\\A(.+)\\-(\\w{10})\\-(\\w{5})\\z`)\n\n// PodToKey converts a pod path to a generic bench config key.\nfunc PodToKey(path string) string {\n\ttokens := strings.Split(path, \"|\")\n\tns, po := client.Namespaced(tokens[0])\n\tsections := podNameRX.FindStringSubmatch(po)\n\tif len(sections) >= 1 {\n\t\tpo = sections[1]\n\t}\n\treturn client.FQN(ns, po) + \":\" + tokens[1]\n}\n\n// BenchConfigFor returns a custom bench spec if defined otherwise returns the default one.\nfunc BenchConfigFor(benchFile, path string) config.BenchConfig {\n\tdef := config.DefaultBenchSpec()\n\tcust, err := config.NewBench(benchFile)\n\tif err != nil {\n\t\tslog.Debug(\"No custom benchmark config file found. Using default\",\n\t\t\tslogs.FileName, benchFile,\n\t\t\tslogs.Error, err,\n\t\t)\n\t\treturn def\n\t}\n\tif b, ok := cust.Benchmarks.Containers[PodToKey(path)]; ok {\n\t\treturn b\n\t}\n\n\tdef.C, def.N = cust.Benchmarks.Defaults.C, cust.Benchmarks.Defaults.N\n\treturn def\n}\n"
  },
  {
    "path": "internal/dao/port_forward_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestBenchForConfig(t *testing.T) {\n\tuu := map[string]struct {\n\t\tfile, key string\n\t\tspec      config.BenchConfig\n\t}{\n\t\t\"no_file\": {file: \"\", key: \"\", spec: config.DefaultBenchSpec()},\n\t\t\"spec\": {file: \"testdata/benchspec.yaml\", key: \"default/nginx-123-456|nginx\", spec: config.BenchConfig{\n\t\t\tC: 2,\n\t\t\tN: 3000,\n\t\t\tHTTP: config.HTTP{\n\t\t\t\tMethod: \"GET\",\n\t\t\t\tPath:   \"/\",\n\t\t\t},\n\t\t}},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.NotNil(t, u.spec, dao.BenchConfigFor(u.file, u.key))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dao/port_forwarder.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/port\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n\t\"k8s.io/apimachinery/pkg/runtime/serializer\"\n\t\"k8s.io/apimachinery/pkg/util/httpstream\"\n\t\"k8s.io/cli-runtime/pkg/genericclioptions\"\n\t\"k8s.io/client-go/rest\"\n\t\"k8s.io/client-go/tools/portforward\"\n\t\"k8s.io/client-go/transport/spdy\"\n\tcmdutil \"k8s.io/kubectl/pkg/cmd/util\"\n)\n\nconst defaultTimeout = 30 * time.Second\n\n// PortForwarder tracks a port forward stream.\ntype PortForwarder struct {\n\tFactory\n\tgenericclioptions.IOStreams\n\n\tstopChan, readyChan chan struct{}\n\tactive              bool\n\tpath                string\n\ttunnel              port.PortTunnel\n\tage                 time.Time\n}\n\n// NewPortForwarder returns a new port forward streamer.\nfunc NewPortForwarder(f Factory) *PortForwarder {\n\treturn &PortForwarder{\n\t\tFactory:   f,\n\t\tstopChan:  make(chan struct{}),\n\t\treadyChan: make(chan struct{}),\n\t}\n}\n\n// String dumps as string.\nfunc (p *PortForwarder) String() string {\n\treturn fmt.Sprintf(\"%s|%s\", p.path, p.tunnel)\n}\n\n// Age returns the port forward age.\nfunc (p *PortForwarder) Age() time.Time {\n\treturn p.age\n}\n\n// Active returns the forward status.\nfunc (p *PortForwarder) Active() bool {\n\treturn p.active\n}\n\n// SetActive mark a portforward as active.\nfunc (p *PortForwarder) SetActive(b bool) {\n\tp.active = b\n}\n\n// Port returns the port mapping.\nfunc (p *PortForwarder) Port() string {\n\treturn p.tunnel.PortMap()\n}\n\n// Address returns the port Address.\nfunc (p *PortForwarder) Address() string {\n\treturn p.tunnel.Address\n}\n\n// ContainerPort returns the container port.\nfunc (p *PortForwarder) ContainerPort() string {\n\treturn p.tunnel.ContainerPort\n}\n\n// LocalPort returns the local port.\nfunc (p *PortForwarder) LocalPort() string {\n\treturn p.tunnel.LocalPort\n}\n\n// ID returns a pf id.\nfunc (p *PortForwarder) ID() string {\n\treturn PortForwardID(p.path, p.tunnel.Container, p.tunnel.PortMap())\n}\n\n// Container returns the target's container.\nfunc (p *PortForwarder) Container() string {\n\treturn p.tunnel.Container\n}\n\n// Stop terminates a port forward.\nfunc (p *PortForwarder) Stop() {\n\tp.active = false\n\tif p.stopChan != nil {\n\t\tclose(p.stopChan)\n\t\tp.stopChan = nil\n\t}\n}\n\n// FQN returns the portforward unique id.\nfunc (p *PortForwarder) FQN() string {\n\treturn p.path + \":\" + p.tunnel.Container\n}\n\n// HasPortMapping checks if port mapping is defined for this fwd.\nfunc (p *PortForwarder) HasPortMapping(portMap string) bool {\n\treturn p.tunnel.PortMap() == portMap\n}\n\n// Start initiates a port forward session for a given pod and ports.\nfunc (p *PortForwarder) Start(path string, tt port.PortTunnel) (*portforward.PortForwarder, error) {\n\tp.path, p.tunnel, p.age = path, tt, time.Now()\n\n\tns, n := client.Namespaced(path)\n\tauth, err := p.Client().CanI(ns, client.PodGVR, n, client.GetAccess)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !auth {\n\t\treturn nil, fmt.Errorf(\"user is not authorized to get pods\")\n\t}\n\n\tpodName := strings.Split(n, \"|\")[0]\n\tvar res Pod\n\tres.Init(p, client.PodGVR)\n\tpod, err := res.GetInstance(client.FQN(ns, podName))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif pod.Status.Phase != v1.PodRunning {\n\t\treturn nil, fmt.Errorf(\"unable to forward port because pod is not running. Current status=%v\", pod.Status.Phase)\n\t}\n\n\tauth, err = p.Client().CanI(ns, client.PodGVR.WithSubResource(\"portforward\"), \"\", []string{client.CreateVerb})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !auth {\n\t\treturn nil, fmt.Errorf(\"user is not authorized to update portforward\")\n\t}\n\n\tcfg, err := p.Client().RestConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcfg.GroupVersion = &schema.GroupVersion{Group: \"\", Version: \"v1\"}\n\tcfg.APIPath = \"/api\"\n\tcodec, _ := codec()\n\tcfg.NegotiatedSerializer = codec.WithoutConversion()\n\tclt, err := rest.RESTClientFor(cfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq := clt.Post().\n\t\tResource(\"pods\").\n\t\tNamespace(ns).\n\t\tName(podName).\n\t\tSubResource(\"portforward\")\n\n\treturn p.forwardPorts(\"POST\", req.URL(), tt.Address, tt.PortMap())\n}\n\nfunc (p *PortForwarder) forwardPorts(method string, u *url.URL, addr, portMap string) (*portforward.PortForwarder, error) {\n\tcfg, err := p.Client().Config().RESTConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\ttransport, upgrader, err := spdy.RoundTripperFor(cfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport, Timeout: defaultTimeout}, method, u)\n\n\tif !cmdutil.PortForwardWebsockets.IsDisabled() {\n\t\ttunnelingDialer, err := portforward.NewSPDYOverWebsocketDialer(u, cfg)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// First attempt tunneling (websocket) dialer, then fallback to spdy dialer.\n\t\tdialer = portforward.NewFallbackDialer(tunnelingDialer, dialer, func(err error) bool {\n\t\t\treturn httpstream.IsUpgradeFailure(err) || httpstream.IsHTTPSProxyError(err)\n\t\t})\n\t}\n\n\treturn portforward.NewOnAddresses(dialer, []string{addr}, []string{portMap}, p.stopChan, p.readyChan, p.Out, p.ErrOut)\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\n// PortForwardID computes port-forward identifier.\nfunc PortForwardID(path, co, portMap string) string {\n\tif strings.Contains(path, \"|\") {\n\t\treturn path + \"|\" + portMap\n\t}\n\n\treturn path + \"|\" + co + \"|\" + portMap\n}\n\nfunc codec() (serializer.CodecFactory, runtime.ParameterCodec) {\n\tscheme := runtime.NewScheme()\n\tgv := schema.GroupVersion{Group: \"\", Version: \"v1\"}\n\tmetav1.AddToGroupVersion(scheme, gv)\n\tscheme.AddKnownTypes(gv, &metav1.Table{}, &metav1.TableOptions{})\n\tscheme.AddKnownTypes(metav1.SchemeGroupVersion, &metav1.Table{}, &metav1.TableOptions{})\n\n\treturn serializer.NewCodecFactory(scheme), runtime.NewParameterCodec(scheme)\n}\n"
  },
  {
    "path": "internal/dao/pulse.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\n// Pulse tracks pulses.\ntype Pulse struct {\n\tNonResource\n}\n\n// List lists out pulses.\nfunc (*Pulse) List(context.Context, string) ([]runtime.Object, error) {\n\treturn nil, fmt.Errorf(\"NYI\")\n}\n"
  },
  {
    "path": "internal/dao/rbac.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\trbacv1 \"k8s.io/api/rbac/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar (\n\t_ Accessor = (*Rbac)(nil)\n\t_ Nuker    = (*Rbac)(nil)\n)\n\n// Rbac represents a model for listing rbac resources.\ntype Rbac struct {\n\tResource\n}\n\n// List lists out rbac resources.\nfunc (r *Rbac) List(ctx context.Context, ns string) ([]runtime.Object, error) {\n\tgvr, ok := ctx.Value(internal.KeyGVR).(*client.GVR)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"expecting a context gvr\")\n\t}\n\tpath, ok := ctx.Value(internal.KeyPath).(string)\n\tif !ok || path == \"\" {\n\t\treturn r.Resource.List(ctx, ns)\n\t}\n\n\tswitch gvr.R() {\n\tcase \"clusterrolebindings\":\n\t\treturn r.loadClusterRoleBinding(path)\n\tcase \"rolebindings\":\n\t\treturn r.loadRoleBinding(path)\n\tcase \"clusterroles\":\n\t\treturn r.loadClusterRole(path)\n\tcase \"roles\":\n\t\treturn r.loadRole(path)\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"expecting clusterrole/role but found %s\", gvr.R())\n\t}\n}\n\nfunc (r *Rbac) loadClusterRoleBinding(path string) ([]runtime.Object, error) {\n\tcrbo, err := r.getFactory().Get(client.CrbGVR, path, true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar crb rbacv1.ClusterRoleBinding\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(crbo.(*unstructured.Unstructured).Object, &crb)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcro, err := r.getFactory().Get(client.CrGVR, client.FQN(\"-\", crb.RoleRef.Name), true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar cr rbacv1.ClusterRole\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(cro.(*unstructured.Unstructured).Object, &cr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn asRuntimeObjects(parseRules(client.ClusterScope, \"-\", cr.Rules)), nil\n}\n\nfunc (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) {\n\trbo, err := r.getFactory().Get(client.RobGVR, path, true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar rb rbacv1.RoleBinding\n\tif e := runtime.DefaultUnstructuredConverter.FromUnstructured(rbo.(*unstructured.Unstructured).Object, &rb); e != nil {\n\t\treturn nil, e\n\t}\n\n\tif rb.RoleRef.Kind == \"ClusterRole\" {\n\t\tcro, e := r.getFactory().Get(client.CrGVR, client.FQN(\"-\", rb.RoleRef.Name), true, labels.Everything())\n\t\tif e != nil {\n\t\t\treturn nil, e\n\t\t}\n\t\tvar cr rbacv1.ClusterRole\n\t\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(cro.(*unstructured.Unstructured).Object, &cr)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn asRuntimeObjects(parseRules(client.ClusterScope, \"-\", cr.Rules)), nil\n\t}\n\n\tro, err := r.getFactory().Get(client.RoGVR, client.FQN(rb.Namespace, rb.RoleRef.Name), true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar role rbacv1.Role\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(ro.(*unstructured.Unstructured).Object, &role)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn asRuntimeObjects(parseRules(client.ClusterScope, \"-\", role.Rules)), nil\n}\n\nfunc (r *Rbac) loadClusterRole(fqn string) ([]runtime.Object, error) {\n\to, err := r.getFactory().Get(client.CrGVR, fqn, true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar cr rbacv1.ClusterRole\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn asRuntimeObjects(parseRules(client.ClusterScope, \"-\", cr.Rules)), nil\n}\n\nfunc (r *Rbac) loadRole(path string) ([]runtime.Object, error) {\n\to, err := r.getFactory().Get(client.RoGVR, path, true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar ro rbacv1.Role\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ro)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn asRuntimeObjects(parseRules(client.ClusterScope, \"-\", ro.Rules)), nil\n}\n\nfunc asRuntimeObjects(rr render.Policies) []runtime.Object {\n\too := make([]runtime.Object, len(rr))\n\tfor i, r := range rr {\n\t\too[i] = r\n\t}\n\n\treturn oo\n}\n\nfunc parseRules(ns, binding string, rules []rbacv1.PolicyRule) render.Policies {\n\tpp := make(render.Policies, 0, len(rules))\n\tfor _, rule := range rules {\n\t\tfor _, grp := range rule.APIGroups {\n\t\t\tif grp == \"\" {\n\t\t\t\tgrp = \"core\"\n\t\t\t}\n\t\t\tfor _, res := range rule.Resources {\n\t\t\t\tfor _, na := range rule.ResourceNames {\n\t\t\t\t\tpp = pp.Upsert(render.NewPolicyRes(ns, binding, FQN(res, na), grp, rule.Verbs))\n\t\t\t\t}\n\t\t\t\tpp = pp.Upsert(render.NewPolicyRes(ns, binding, FQN(grp, res), grp, rule.Verbs))\n\t\t\t}\n\t\t}\n\t\tfor _, nres := range rule.NonResourceURLs {\n\t\t\tif nres[0] != '/' {\n\t\t\t\tnres = \"/\" + nres\n\t\t\t}\n\t\t\tpp = pp.Upsert(render.NewPolicyRes(ns, binding, nres, client.NA, rule.Verbs))\n\t\t}\n\t}\n\n\treturn pp\n}\n"
  },
  {
    "path": "internal/dao/rbac_policy.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\trbacv1 \"k8s.io/api/rbac/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar (\n\t_ Accessor = (*Policy)(nil)\n\t_ Nuker    = (*Policy)(nil)\n)\n\n// Policy represent rbac policy.\ntype Policy struct {\n\tResource\n}\n\n// List returns available policies.\nfunc (p *Policy) List(ctx context.Context, _ string) ([]runtime.Object, error) {\n\tkind, ok := ctx.Value(internal.KeySubjectKind).(string)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"expecting a context subject kind\")\n\t}\n\tname, ok := ctx.Value(internal.KeySubjectName).(string)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"expecting a context subject name\")\n\t}\n\n\tcrps, err := p.loadClusterRoleBinding(kind, name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trps, err := p.loadRoleBinding(kind, name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\too := make([]runtime.Object, 0, len(crps)+len(rps))\n\tfor _, p := range crps {\n\t\too = append(oo, p)\n\t}\n\tfor _, p := range rps {\n\t\too = append(oo, p)\n\t}\n\n\treturn oo, nil\n}\n\nfunc (p *Policy) loadClusterRoleBinding(kind, name string) (render.Policies, error) {\n\tcrbs, err := fetchClusterRoleBindings(p.Factory)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tns, n := client.Namespaced(name)\n\tvar nn []string\n\tfor i := range crbs {\n\t\tfor _, s := range crbs[i].Subjects {\n\t\t\tif isSameSubject(kind, ns, crbs[i].Namespace, n, &s) {\n\t\t\t\tnn = append(nn, crbs[i].RoleRef.Name)\n\t\t\t}\n\t\t}\n\t}\n\tcrs, err := p.fetchClusterRoles()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trows := make(render.Policies, 0, len(nn))\n\tfor i := range crs {\n\t\tif !inList(nn, crs[i].Name) {\n\t\t\tcontinue\n\t\t}\n\t\trows = append(rows, parseRules(client.NotNamespaced, \"CR:\"+crs[i].Name, crs[i].Rules)...)\n\t}\n\n\treturn rows, nil\n}\n\nfunc (p *Policy) loadRoleBinding(kind, name string) (render.Policies, error) {\n\trbsMap, err := p.fetchRoleBindingNamespaces(kind, name)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcrs, err := p.fetchClusterRoles()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\trows := make(render.Policies, 0, len(crs))\n\tfor i := range crs {\n\t\tif rbNs, ok := rbsMap[\"ClusterRole:\"+crs[i].Name]; ok {\n\t\t\tslog.Debug(\"Loading rules for clusterrole\",\n\t\t\t\tslogs.Namespace, rbNs,\n\t\t\t\tslogs.ResName, crs[i].Name,\n\t\t\t)\n\t\t\trows = append(rows, parseRules(rbNs, \"CR:\"+crs[i].Name, crs[i].Rules)...)\n\t\t}\n\t}\n\n\tros, err := p.fetchRoles()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor i := range ros {\n\t\tif _, ok := rbsMap[\"Role:\"+ros[i].Name]; !ok {\n\t\t\tcontinue\n\t\t}\n\t\tslog.Debug(\"Loading rules for role\",\n\t\t\tslogs.Namespace, ros[i].Namespace,\n\t\t\tslogs.ResName, ros[i].Name,\n\t\t)\n\t\trows = append(rows, parseRules(ros[i].Namespace, \"RO:\"+ros[i].Name, ros[i].Rules)...)\n\t}\n\n\treturn rows, nil\n}\n\nfunc fetchClusterRoleBindings(f Factory) ([]rbacv1.ClusterRoleBinding, error) {\n\too, err := f.List(client.CrbGVR, client.ClusterScope, false, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcrbs := make([]rbacv1.ClusterRoleBinding, len(oo))\n\tfor i, o := range oo {\n\t\tvar crb rbacv1.ClusterRoleBinding\n\t\tif e := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &crb); e != nil {\n\t\t\treturn nil, e\n\t\t}\n\t\tcrbs[i] = crb\n\t}\n\n\treturn crbs, nil\n}\n\nfunc fetchRoleBindings(f Factory) ([]rbacv1.RoleBinding, error) {\n\too, err := f.List(client.RobGVR, client.ClusterScope, false, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trbs := make([]rbacv1.RoleBinding, 0, len(oo))\n\tfor _, o := range oo {\n\t\tvar rb rbacv1.RoleBinding\n\t\tif e := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rb); e != nil {\n\t\t\treturn nil, e\n\t\t}\n\t\trbs = append(rbs, rb)\n\t}\n\n\treturn rbs, nil\n}\n\nfunc (p *Policy) fetchRoleBindingNamespaces(kind, name string) (map[string]string, error) {\n\trbs, err := fetchRoleBindings(p.Factory)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tns, n := client.Namespaced(name)\n\tss := make(map[string]string, len(rbs))\n\tfor i := range rbs {\n\t\tfor _, s := range rbs[i].Subjects {\n\t\t\tif isSameSubject(kind, ns, rbs[i].Namespace, n, &s) {\n\t\t\t\tss[rbs[i].RoleRef.Kind+\":\"+rbs[i].RoleRef.Name] = rbs[i].Namespace\n\t\t\t}\n\t\t}\n\t}\n\n\treturn ss, nil\n}\n\n// isSameSubject verifies if the incoming type name and namespace match a subject from a\n// cluster/roleBinding. A ServiceAccount will always have a namespace and needs to be validated to ensure\n// we don't display permissions for a ServiceAccount with the same name in a different namespace\nfunc isSameSubject(kind, ns, bns, name string, subject *rbacv1.Subject) bool {\n\tif subject.Kind != kind || subject.Name != name {\n\t\treturn false\n\t}\n\tif kind == rbacv1.ServiceAccountKind {\n\t\t// Kind and name were checked above, check the namespace\n\t\tcns := subject.Namespace\n\t\tif cns == \"\" {\n\t\t\tcns = bns\n\t\t}\n\t\treturn client.IsAllNamespaces(ns) || cns == ns\n\t}\n\treturn true\n}\n\nfunc (p *Policy) fetchClusterRoles() ([]rbacv1.ClusterRole, error) {\n\too, err := p.getFactory().List(client.CrGVR, client.ClusterScope, false, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcrs := make([]rbacv1.ClusterRole, len(oo))\n\tfor i, o := range oo {\n\t\tvar cr rbacv1.ClusterRole\n\t\tif e := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cr); e != nil {\n\t\t\treturn nil, e\n\t\t}\n\t\tcrs[i] = cr\n\t}\n\n\treturn crs, nil\n}\n\nfunc (p *Policy) fetchRoles() ([]rbacv1.Role, error) {\n\too, err := p.getFactory().List(client.RoGVR, client.BlankNamespace, false, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trr := make([]rbacv1.Role, len(oo))\n\tfor i, o := range oo {\n\t\tvar ro rbacv1.Role\n\t\tif err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &ro); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\trr[i] = ro\n\t}\n\n\treturn rr, nil\n}\n"
  },
  {
    "path": "internal/dao/rbac_policy_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\trbacv1 \"k8s.io/api/rbac/v1\"\n)\n\nfunc TestIsSameSubject(t *testing.T) {\n\tuu := map[string]struct {\n\t\tkind      string\n\t\tnamespace string\n\t\tname      string\n\t\tsubject   rbacv1.Subject\n\t\twant      bool\n\t}{\n\t\t\"kind-name-match\": {\n\t\t\tkind: rbacv1.UserKind,\n\t\t\tname: \"foo\",\n\t\t\tsubject: rbacv1.Subject{\n\t\t\t\tKind: rbacv1.UserKind,\n\t\t\t\tName: \"foo\",\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\n\t\t\"name-does-not-match\": {\n\t\t\tkind: rbacv1.UserKind,\n\t\t\tname: \"foo\",\n\t\t\tsubject: rbacv1.Subject{\n\t\t\t\tKind: rbacv1.UserKind,\n\t\t\t\tName: \"bar\",\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\n\t\t\"kind-does-not-match\": {\n\t\t\tkind: rbacv1.GroupKind,\n\t\t\tname: \"foo\",\n\t\t\tsubject: rbacv1.Subject{\n\t\t\t\tKind: rbacv1.UserKind,\n\t\t\t\tName: \"foo\",\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\n\t\t\"serviceAccount-all-match\": {\n\t\t\tkind:      rbacv1.ServiceAccountKind,\n\t\t\tname:      \"foo\",\n\t\t\tnamespace: \"bar\",\n\t\t\tsubject: rbacv1.Subject{\n\t\t\t\tKind:      rbacv1.ServiceAccountKind,\n\t\t\t\tName:      \"foo\",\n\t\t\t\tNamespace: \"bar\",\n\t\t\t},\n\t\t\twant: true,\n\t\t},\n\n\t\t\"serviceAccount-namespace-no-match\": {\n\t\t\tkind:      rbacv1.ServiceAccountKind,\n\t\t\tname:      \"foo\",\n\t\t\tnamespace: \"bar\",\n\t\t\tsubject: rbacv1.Subject{\n\t\t\t\tKind:      rbacv1.ServiceAccountKind,\n\t\t\t\tName:      \"foo\",\n\t\t\t\tNamespace: \"bazz\",\n\t\t\t},\n\t\t\twant: false,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tsame := isSameSubject(u.kind, u.namespace, u.namespace, u.name, &u.subject)\n\t\t\tassert.Equal(t, u.want, same)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dao/rbac_subject.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar (\n\t_ Accessor = (*Subject)(nil)\n\t_ Nuker    = (*Subject)(nil)\n)\n\n// Subject represents a subject model.\ntype Subject struct {\n\tResource\n}\n\n// List returns a collection of subjects.\nfunc (s *Subject) List(ctx context.Context, _ string) ([]runtime.Object, error) {\n\tkind, ok := ctx.Value(internal.KeySubjectKind).(string)\n\tif !ok {\n\t\treturn nil, errors.New(\"expecting a SubjectKind\")\n\t}\n\n\tcrbs, err := s.listClusterRoleBindings(kind)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trbs, err := s.listRoleBindings(kind)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, rb := range rbs {\n\t\tcrbs = crbs.Upsert(rb)\n\t}\n\n\too := make([]runtime.Object, len(crbs))\n\tfor i, o := range crbs {\n\t\too[i] = o\n\t}\n\treturn oo, nil\n}\n\nfunc (s *Subject) listClusterRoleBindings(kind string) (render.Subjects, error) {\n\tcrbs, err := fetchClusterRoleBindings(s.Factory)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\too := make(render.Subjects, 0, len(crbs))\n\tfor i := range crbs {\n\t\tfor _, su := range crbs[i].Subjects {\n\t\t\tif su.Kind != kind {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\too = oo.Upsert(render.SubjectRes{\n\t\t\t\tName:          su.Name,\n\t\t\t\tKind:          \"ClusterRoleBinding\",\n\t\t\t\tFirstLocation: crbs[i].Name,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn oo, nil\n}\n\nfunc (s *Subject) listRoleBindings(kind string) (render.Subjects, error) {\n\trbs, err := fetchRoleBindings(s.Factory)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\too := make(render.Subjects, 0, len(rbs))\n\tfor i := range rbs {\n\t\tfor _, su := range rbs[i].Subjects {\n\t\t\tif su.Kind != kind {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\too = oo.Upsert(render.SubjectRes{\n\t\t\t\tName:          su.Name,\n\t\t\t\tKind:          \"RoleBinding\",\n\t\t\t\tFirstLocation: rbs[i].Name,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn oo, nil\n}\n"
  },
  {
    "path": "internal/dao/recorder.go",
    "content": "package dao\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/util/cache\"\n)\n\nvar MxRecorder *Recorder\n\nconst (\n\tseriesCacheSize   = 600\n\tseriesCacheExpiry = 3 * time.Hour\n\tseriesRecordRate  = 1 * time.Minute\n\tnodeMetrics       = \"node\"\n\tpodMetrics        = \"pod\"\n)\n\ntype MetricsChan chan TimeSeries\n\ntype TimeSeries []Point\n\ntype Point struct {\n\tTime  time.Time\n\tTags  map[string]string\n\tValue client.NodeMetrics\n}\n\ntype Recorder struct {\n\tconn   client.Connection\n\tseries *cache.LRUExpireCache\n\tmxChan MetricsChan\n\tmx     sync.RWMutex\n}\n\nfunc DialRecorder(c client.Connection) *Recorder {\n\tif MxRecorder != nil {\n\t\treturn MxRecorder\n\t}\n\tMxRecorder = &Recorder{\n\t\tconn:   c,\n\t\tseries: cache.NewLRUExpireCache(seriesCacheSize),\n\t}\n\n\treturn MxRecorder\n}\n\nfunc ResetRecorder(c client.Connection) {\n\tMxRecorder = nil\n\tDialRecorder(c)\n}\n\nfunc (r *Recorder) Clear() {\n\tr.mx.Lock()\n\tdefer r.mx.Unlock()\n\n\tkk := r.series.Keys()\n\tfor _, k := range kk {\n\t\tr.series.Remove(k)\n\t}\n}\n\nfunc (r *Recorder) dispatchSeries(kind, ns string) {\n\tif r.mxChan == nil {\n\t\treturn\n\t}\n\tkk := r.series.Keys()\n\thour := time.Now().Add(-1 * time.Hour)\n\tts := make(TimeSeries, 0, len(kk))\n\tfor _, k := range kk {\n\t\tif v, ok := r.series.Get(k); ok {\n\t\t\tif pt, cool := v.(Point); cool {\n\t\t\t\tif pt.Tags[\"type\"] != kind || pt.Time.Sub(hour) < 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tswitch kind {\n\t\t\t\tcase nodeMetrics:\n\t\t\t\t\tts = append(ts, pt)\n\t\t\t\tcase podMetrics:\n\t\t\t\t\tif client.IsAllNamespaces(ns) || pt.Tags[\"namespace\"] == ns {\n\t\t\t\t\t\tts = append(ts, pt)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\tif len(ts) > 0 {\n\t\tr.mxChan <- ts\n\t}\n}\n\nfunc (r *Recorder) Watch(ctx context.Context, ns string) MetricsChan {\n\tr.mx.Lock()\n\tif r.mxChan != nil {\n\t\tclose(r.mxChan)\n\t\tr.mxChan = nil\n\t}\n\tr.mxChan = make(MetricsChan, 2)\n\tr.mx.Unlock()\n\n\tgo func() {\n\t\tkind := podMetrics\n\t\tif client.IsAllNamespaces(ns) {\n\t\t\tkind = nodeMetrics\n\t\t}\n\t\tswitch kind {\n\t\tcase podMetrics:\n\t\t\tif err := r.recordPodMetrics(ctx, ns); err != nil {\n\t\t\t\tslog.Error(\"Record pod metrics failed\", slogs.Error, err)\n\t\t\t}\n\t\tcase nodeMetrics:\n\t\t\tif err := r.recordNodeMetrics(ctx); err != nil {\n\t\t\t\tslog.Error(\"Record node metrics failed\", slogs.Error, err)\n\t\t\t}\n\t\t}\n\t\tr.dispatchSeries(kind, ns)\n\t\t<-ctx.Done()\n\t\tr.mx.Lock()\n\t\tif r.mxChan != nil {\n\t\t\tclose(r.mxChan)\n\t\t\tr.mxChan = nil\n\t\t}\n\t\tr.mx.Unlock()\n\t}()\n\n\treturn r.mxChan\n}\n\nfunc (r *Recorder) Record(ctx context.Context) error {\n\tif err := r.recordNodeMetrics(ctx); err != nil {\n\t\treturn err\n\t}\n\treturn r.recordPodMetrics(ctx, client.NamespaceAll)\n}\n\nfunc (r *Recorder) recordNodeMetrics(ctx context.Context) error {\n\tf, ok := ctx.Value(internal.KeyFactory).(Factory)\n\tif !ok {\n\t\treturn errors.New(\"expecting factory in context\")\n\t}\n\tnn, err := FetchNodes(ctx, f, \"\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tgo func() {\n\t\tr.recordClusterMetrics(ctx, nn)\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\tcase <-time.After(seriesRecordRate):\n\t\t\t\tr.recordClusterMetrics(ctx, nn)\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn nil\n}\n\nfunc (r *Recorder) recordClusterMetrics(ctx context.Context, nn *v1.NodeList) {\n\tdial := client.DialMetrics(r.conn)\n\tnmx, err := dial.FetchNodesMetrics(ctx)\n\tif err != nil {\n\t\tslog.Error(\"Fetch node metrics failed\", slogs.Error, err)\n\t\treturn\n\t}\n\n\tmx := make(client.NodesMetrics, len(nn.Items))\n\tdial.NodesMetrics(nn, nmx, mx)\n\tvar cmx client.NodeMetrics\n\tfor _, m := range mx {\n\t\tcmx.CurrentCPU += m.CurrentCPU\n\t\tcmx.CurrentMEM += m.CurrentMEM\n\t\tcmx.AllocatableCPU += m.AllocatableCPU\n\t\tcmx.AllocatableMEM += m.AllocatableMEM\n\t\tcmx.TotalCPU += m.TotalCPU\n\t\tcmx.TotalMEM += m.TotalMEM\n\t}\n\tpt := Point{\n\t\tTime:  time.Now(),\n\t\tValue: cmx,\n\t\tTags: map[string]string{\n\t\t\t\"type\": nodeMetrics,\n\t\t},\n\t}\n\tif len(nn.Items) > 0 {\n\t\tr.series.Add(pt.Time, pt, seriesCacheExpiry)\n\t}\n\tr.mx.Lock()\n\tdefer r.mx.Unlock()\n\tif r.mxChan != nil {\n\t\tr.mxChan <- TimeSeries{pt}\n\t}\n}\n\nfunc (r *Recorder) recordPodMetrics(ctx context.Context, ns string) error {\n\tgo func() {\n\t\tif err := r.recordPodsMetrics(ctx, ns); err != nil {\n\t\t\tslog.Error(\"Record pod metrics failed\", slogs.Error, err)\n\t\t}\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn\n\t\t\tcase <-time.After(seriesRecordRate):\n\t\t\t\t// case <-time.After(5 * time.Second):\n\t\t\t\tif err := r.recordPodsMetrics(ctx, ns); err != nil {\n\t\t\t\t\tslog.Error(\"Record pod metrics failed\", slogs.Error, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn nil\n}\n\nfunc (r *Recorder) recordPodsMetrics(ctx context.Context, ns string) error {\n\tf, ok := ctx.Value(internal.KeyFactory).(Factory)\n\tif !ok {\n\t\treturn errors.New(\"expecting factory in context\")\n\t}\n\tpp, err := FetchPods(ctx, f, ns)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpt := Point{\n\t\tTime:  time.Now(),\n\t\tValue: client.NodeMetrics{},\n\t\tTags: map[string]string{\n\t\t\t\"namespace\": ns,\n\t\t\t\"type\":      podMetrics,\n\t\t},\n\t}\n\tdial := client.DialMetrics(r.conn)\n\tfor i := range pp.Items {\n\t\tp := pp.Items[i]\n\t\tfqn := client.FQN(p.Namespace, p.Name)\n\t\tpmx, err := dial.FetchPodMetrics(ctx, fqn)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tfor _, c := range pmx.Containers {\n\t\t\tpt.Value.CurrentCPU += c.Usage.Cpu().MilliValue()\n\t\t\tpt.Value.CurrentMEM += client.ToMB(c.Usage.Memory().Value())\n\t\t}\n\t}\n\tif len(pp.Items) > 0 {\n\t\tpt.Value.AllocatableCPU = pt.Value.CurrentCPU\n\t\tpt.Value.AllocatableMEM = pt.Value.CurrentMEM\n\t\tr.series.Add(pt.Time, pt, seriesCacheExpiry)\n\t\tr.mx.Lock()\n\t\tdefer r.mx.Unlock()\n\t\tif r.mxChan != nil {\n\t\t\tr.mxChan <- TimeSeries{pt}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// FetchPods retrieves all pods in a given namespace.\nfunc FetchPods(_ context.Context, f Factory, ns string) (*v1.PodList, error) {\n\tauth, err := f.Client().CanI(ns, client.PodGVR, \"pods\", []string{client.ListVerb})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !auth {\n\t\treturn nil, fmt.Errorf(\"user is not authorized to list pods\")\n\t}\n\n\too, err := f.List(client.PodGVR, ns, false, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpp := make([]v1.Pod, 0, len(oo))\n\tfor _, o := range oo {\n\t\tvar pod v1.Pod\n\t\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpp = append(pp, pod)\n\t}\n\n\treturn &v1.PodList{Items: pp}, nil\n}\n"
  },
  {
    "path": "internal/dao/reference.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"errors\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar _ Accessor = (*Reference)(nil)\n\n// Reference represents cluster resource references.\ntype Reference struct {\n\tNonResource\n}\n\n// List collects all references.\nfunc (r *Reference) List(ctx context.Context, _ string) ([]runtime.Object, error) {\n\tgvr, ok := ctx.Value(internal.KeyGVR).(*client.GVR)\n\tif !ok {\n\t\treturn nil, errors.New(\"no context for gvr found\")\n\t}\n\tswitch gvr {\n\tcase client.SaGVR:\n\t\treturn r.ScanSA(ctx)\n\tdefault:\n\t\treturn r.Scan(ctx)\n\t}\n}\n\n// Get fetch a given reference.\nfunc (*Reference) Get(context.Context, string) (runtime.Object, error) {\n\tpanic(\"NYI\")\n}\n\n// Scan scan cluster resources for references.\nfunc (r *Reference) Scan(ctx context.Context) ([]runtime.Object, error) {\n\trefs, err := ScanForRefs(ctx, r.Factory)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfqn, ok := ctx.Value(internal.KeyPath).(string)\n\tif !ok {\n\t\treturn nil, errors.New(\"expecting context Path\")\n\t}\n\tns, _ := client.Namespaced(fqn)\n\too := make([]runtime.Object, 0, len(refs))\n\tfor _, ref := range refs {\n\t\t_, n := client.Namespaced(ref.FQN)\n\t\too = append(oo, render.ReferenceRes{\n\t\t\tNamespace: ns,\n\t\t\tName:      n,\n\t\t\tGVR:       ref.GVR,\n\t\t})\n\t}\n\n\treturn oo, nil\n}\n\n// ScanSA scans for serviceaccount refs.\nfunc (r *Reference) ScanSA(ctx context.Context) ([]runtime.Object, error) {\n\trefs, err := ScanForSARefs(ctx, r.Factory)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfqn, ok := ctx.Value(internal.KeyPath).(string)\n\tif !ok {\n\t\treturn nil, errors.New(\"expecting context Path\")\n\t}\n\tns, _ := client.Namespaced(fqn)\n\too := make([]runtime.Object, 0, len(refs))\n\tfor _, ref := range refs {\n\t\t_, n := client.Namespaced(ref.FQN)\n\t\too = append(oo, render.ReferenceRes{\n\t\t\tNamespace: ns,\n\t\t\tName:      n,\n\t\t\tGVR:       ref.GVR,\n\t\t})\n\t}\n\n\treturn oo, nil\n}\n"
  },
  {
    "path": "internal/dao/registry.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"maps\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\tapiext \"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n\t\"k8s.io/apimachinery/pkg/util/sets\"\n)\n\nconst (\n\tcrdCat   = \"crd\"\n\tk9sCat   = \"k9s\"\n\thelmCat  = \"helm\"\n\tscaleCat = \"scale\"\n)\n\nvar stdGroups = sets.New[string](\n\t\"apps/v1\",\n\t\"autoscaling/v1\",\n\t\"autoscaling/v2\",\n\t\"autoscaling/v2beta1\",\n\t\"autoscaling/v2beta2\",\n\t\"batch/v1\",\n\t\"batch/v1beta1\",\n\t\"extensions/v1beta1\",\n\t\"policy/v1beta1\",\n\t\"policy/v1\",\n\t\"v1\",\n)\n\nvar scalableRes = sets.New(client.DpGVR, client.StsGVR, client.RsGVR, client.RcGVR)\n\n// ResourceMetas represents a collection of resource metadata.\ntype ResourceMetas map[*client.GVR]*metav1.APIResource\n\nfunc (m ResourceMetas) clear() {\n\tfor k := range m {\n\t\tdelete(m, k)\n\t}\n}\n\n// MetaAccess tracks resources metadata.\nvar MetaAccess = NewMeta()\n\n// Meta represents available resource metas.\ntype Meta struct {\n\tresMetas ResourceMetas\n\tmx       sync.RWMutex\n}\n\n// NewMeta returns a resource meta.\nfunc NewMeta() *Meta {\n\treturn &Meta{resMetas: make(ResourceMetas)}\n}\n\nfunc (m *Meta) Lookup(cmd string) *client.GVR {\n\tm.mx.RLock()\n\tdefer m.mx.RUnlock()\n\tfor gvr, meta := range m.resMetas {\n\t\tif slices.Contains(meta.ShortNames, cmd) {\n\t\t\treturn gvr\n\t\t}\n\t\tif meta.Name == cmd || meta.SingularName == cmd || meta.Kind == cmd {\n\t\t\treturn gvr\n\t\t}\n\t}\n\n\treturn client.NoGVR\n}\n\n// RegisterMeta registers a new resource meta object.\nfunc (m *Meta) RegisterMeta(gvr string, res *metav1.APIResource) {\n\tm.mx.Lock()\n\tdefer m.mx.Unlock()\n\n\tm.resMetas[client.NewGVR(gvr)] = res\n}\n\n// AllGVRs returns all sorted cluster resources.\nfunc (m *Meta) AllGVRs() client.GVRs {\n\tm.mx.RLock()\n\tdefer m.mx.RUnlock()\n\tkk := slices.Collect(maps.Keys(m.resMetas))\n\n\treturn client.GVRs(kk)\n}\n\n// GVK2GVR convert gvk to gvr\nfunc (m *Meta) GVK2GVR(gv schema.GroupVersion, kind string) (*client.GVR, bool, bool) {\n\tm.mx.RLock()\n\tdefer m.mx.RUnlock()\n\n\tfor gvr, meta := range m.resMetas {\n\t\tif gv.Group == meta.Group && gv.Version == meta.Version && kind == meta.Kind {\n\t\t\treturn gvr, meta.Namespaced, true\n\t\t}\n\t}\n\n\treturn client.NoGVR, false, false\n}\n\n// IsNamespaced checks if a given resource is namespaced.\nfunc (m *Meta) IsNamespaced(gvr *client.GVR) (bool, error) {\n\tres, err := m.MetaFor(gvr)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\treturn res.Namespaced, nil\n}\n\n// MetaFor returns a resource metadata for a given gvr.\nfunc (m *Meta) MetaFor(gvr *client.GVR) (*metav1.APIResource, error) {\n\tm.mx.RLock()\n\tdefer m.mx.RUnlock()\n\n\tif meta, ok := m.resMetas[gvr]; ok {\n\t\treturn meta, nil\n\t}\n\n\treturn new(metav1.APIResource), fmt.Errorf(\"no resource meta defined for\\n %q\", gvr)\n}\n\n// IsCRD checks if resource represents a CRD\nfunc IsCRD(r *metav1.APIResource) bool {\n\treturn slices.Contains(r.Categories, crdCat)\n}\n\n// IsK8sMeta checks for non resource meta.\nfunc IsK8sMeta(m *metav1.APIResource) bool {\n\treturn !slices.ContainsFunc(m.Categories, func(category string) bool {\n\t\treturn category == k9sCat || category == helmCat\n\t})\n}\n\n// IsK9sMeta checks for non resource meta.\nfunc IsK9sMeta(m *metav1.APIResource) bool {\n\treturn slices.Contains(m.Categories, k9sCat)\n}\n\n// IsScalable check if the resource can be scaled\nfunc IsScalable(m *metav1.APIResource) bool {\n\treturn slices.Contains(m.Categories, scaleCat)\n}\n\n// LoadResources hydrates server preferred+CRDs resource metadata.\nfunc (m *Meta) LoadResources(f Factory) error {\n\tm.mx.Lock()\n\tdefer m.mx.Unlock()\n\n\tm.resMetas.clear()\n\tif err := loadPreferred(f, m.resMetas); err != nil {\n\t\treturn err\n\t}\n\tloadNonResource(m.resMetas)\n\n\t// We've actually loaded all the CRDs in loadPreferred, and we're now adding\n\t// some additional CRD properties on top of that.\n\tloadCRDs(f, m.resMetas)\n\n\treturn nil\n}\n\n// BOZO!! Need countermeasures for direct commands!\nfunc loadNonResource(m ResourceMetas) {\n\tloadK9s(m)\n\tloadRBAC(m)\n\tloadHelm(m)\n}\n\nfunc loadK9s(m ResourceMetas) {\n\tm[client.WkGVR] = &metav1.APIResource{\n\t\tName:         \"workloads\",\n\t\tKind:         \"Workload\",\n\t\tSingularName: \"workload\",\n\t\tNamespaced:   true,\n\t\tShortNames:   []string{\"wk\"},\n\t\tCategories:   []string{k9sCat},\n\t}\n\tm[client.PuGVR] = &metav1.APIResource{\n\t\tName:         \"pulses\",\n\t\tKind:         \"Pulse\",\n\t\tSingularName: \"pulse\",\n\t\tShortNames:   []string{\"hz\", \"pu\"},\n\t\tCategories:   []string{k9sCat},\n\t}\n\tm[client.DirGVR] = &metav1.APIResource{\n\t\tName:         \"dirs\",\n\t\tKind:         \"Dir\",\n\t\tSingularName: \"dir\",\n\t\tCategories:   []string{k9sCat},\n\t}\n\tm[client.XGVR] = &metav1.APIResource{\n\t\tName:         \"xrays\",\n\t\tKind:         \"XRays\",\n\t\tSingularName: \"xray\",\n\t\tCategories:   []string{k9sCat},\n\t}\n\tm[client.RefGVR] = &metav1.APIResource{\n\t\tName:         \"references\",\n\t\tKind:         \"References\",\n\t\tSingularName: \"reference\",\n\t\tVerbs:        []string{},\n\t\tCategories:   []string{k9sCat},\n\t}\n\tm[client.AliGVR] = &metav1.APIResource{\n\t\tName:         \"aliases\",\n\t\tKind:         \"Aliases\",\n\t\tSingularName: \"alias\",\n\t\tVerbs:        []string{},\n\t\tCategories:   []string{k9sCat},\n\t}\n\tm[client.CtGVR] = &metav1.APIResource{\n\t\tName:         client.CtGVR.String(),\n\t\tKind:         \"Contexts\",\n\t\tSingularName: \"context\",\n\t\tShortNames:   []string{\"ctx\"},\n\t\tVerbs:        []string{},\n\t\tCategories:   []string{k9sCat},\n\t}\n\tm[client.SdGVR] = &metav1.APIResource{\n\t\tName:         \"screendumps\",\n\t\tKind:         \"ScreenDumps\",\n\t\tSingularName: \"screendump\",\n\t\tShortNames:   []string{\"sd\"},\n\t\tVerbs:        []string{\"delete\"},\n\t\tCategories:   []string{k9sCat},\n\t}\n\tm[client.BeGVR] = &metav1.APIResource{\n\t\tName:         \"benchmarks\",\n\t\tKind:         \"Benchmarks\",\n\t\tSingularName: \"benchmark\",\n\t\tShortNames:   []string{\"be\"},\n\t\tVerbs:        []string{\"delete\"},\n\t\tCategories:   []string{k9sCat},\n\t}\n\tm[client.PfGVR] = &metav1.APIResource{\n\t\tName:         \"portforwards\",\n\t\tNamespaced:   true,\n\t\tKind:         \"PortForwards\",\n\t\tSingularName: \"portforward\",\n\t\tShortNames:   []string{\"pf\"},\n\t\tVerbs:        []string{\"delete\"},\n\t\tCategories:   []string{k9sCat},\n\t}\n\tm[client.CoGVR] = &metav1.APIResource{\n\t\tName:         \"containers\",\n\t\tKind:         \"Containers\",\n\t\tSingularName: \"container\",\n\t\tVerbs:        []string{},\n\t\tCategories:   []string{k9sCat},\n\t}\n\tm[client.ScnGVR] = &metav1.APIResource{\n\t\tName:         \"scans\",\n\t\tKind:         \"Scans\",\n\t\tSingularName: \"scan\",\n\t\tVerbs:        []string{},\n\t\tCategories:   []string{k9sCat},\n\t}\n}\n\nfunc loadHelm(m ResourceMetas) {\n\tm[client.HmGVR] = &metav1.APIResource{\n\t\tName:       \"helm\",\n\t\tKind:       \"Helm\",\n\t\tNamespaced: true,\n\t\tVerbs:      []string{\"delete\"},\n\t\tCategories: []string{helmCat},\n\t}\n\tm[client.HmhGVR] = &metav1.APIResource{\n\t\tName:       \"history\",\n\t\tKind:       \"History\",\n\t\tNamespaced: true,\n\t\tVerbs:      []string{\"delete\"},\n\t\tCategories: []string{helmCat},\n\t}\n}\n\nfunc loadRBAC(m ResourceMetas) {\n\tm[client.RbacGVR] = &metav1.APIResource{\n\t\tName:       \"rbacs\",\n\t\tKind:       \"Rules\",\n\t\tCategories: []string{k9sCat},\n\t}\n\tm[client.PolGVR] = &metav1.APIResource{\n\t\tName:       \"policies\",\n\t\tKind:       \"Rules\",\n\t\tNamespaced: true,\n\t\tCategories: []string{k9sCat},\n\t}\n\tm[client.UsrGVR] = &metav1.APIResource{\n\t\tName:       \"users\",\n\t\tKind:       \"User\",\n\t\tCategories: []string{k9sCat},\n\t}\n\tm[client.GrpGVR] = &metav1.APIResource{\n\t\tName:       \"groups\",\n\t\tKind:       \"Group\",\n\t\tCategories: []string{k9sCat},\n\t}\n}\n\nfunc loadPreferred(f Factory, m ResourceMetas) error {\n\tif f == nil || f.Client() == nil || !f.Client().ConnectionOK() {\n\t\tslog.Error(\"Load cluster resources - No API server connection\")\n\t\treturn nil\n\t}\n\n\tdial, err := f.Client().CachedDiscovery()\n\tif err != nil {\n\t\treturn err\n\t}\n\trr, err := dial.ServerPreferredResources()\n\tif err != nil {\n\t\tslog.Debug(\"Failed to load preferred resources\", slogs.Error, err)\n\t}\n\tfor _, r := range rr {\n\t\tfor i := range r.APIResources {\n\t\t\tres := r.APIResources[i]\n\t\t\tgvr := client.FromGVAndR(r.GroupVersion, res.Name)\n\t\t\tif isDeprecated(gvr) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tres.Group, res.Version = gvr.G(), gvr.V()\n\t\t\tif res.SingularName == \"\" {\n\t\t\t\tres.SingularName = strings.ToLower(res.Kind)\n\t\t\t}\n\t\t\tif !isStandardGroup(r.GroupVersion) {\n\t\t\t\tres.Categories = append(res.Categories, crdCat)\n\t\t\t}\n\t\t\tif isScalable(gvr) {\n\t\t\t\tres.Categories = append(res.Categories, scaleCat)\n\t\t\t}\n\t\t\tm[gvr] = &res\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc isStandardGroup(gv string) bool {\n\treturn stdGroups.Has(gv) || strings.Contains(gv, \".k8s.io\")\n}\n\nfunc isScalable(gvr *client.GVR) bool {\n\treturn scalableRes.Has(gvr)\n}\n\nvar deprecatedGVRs = sets.New(\n\tclient.NewGVR(\"v1/events\"),\n\tclient.NewGVR(\"extensions/v1beta1/ingresses\"),\n)\n\nfunc isDeprecated(gvr *client.GVR) bool {\n\treturn deprecatedGVRs.Has(gvr) || gvr.V() == \"\"\n}\n\n// loadCRDs Wait for the cache to synced and then add some additional properties to CRD.\nfunc loadCRDs(f Factory, m ResourceMetas) {\n\tif f == nil || f.Client() == nil || !f.Client().ConnectionOK() {\n\t\treturn\n\t}\n\n\too, err := f.List(client.CrdGVR, client.ClusterScope, true, labels.Everything())\n\tif err != nil {\n\t\tslog.Warn(\"CRDs load Fail\", slogs.Error, err)\n\t\treturn\n\t}\n\n\tfor _, o := range oo {\n\t\tvar crd apiext.CustomResourceDefinition\n\t\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &crd)\n\t\tif err != nil {\n\t\t\tslog.Error(\"CRD conversion failed\", slogs.Error, err)\n\t\t\tcontinue\n\t\t}\n\t\tfor gvr, version := range client.NewGVRFromCRD(&crd) {\n\t\t\tif meta, ok := m[gvr]; ok && version.Subresources != nil && version.Subresources.Scale != nil {\n\t\t\t\tif !slices.Contains(meta.Categories, scaleCat) {\n\t\t\t\t\tmeta.Categories = append(meta.Categories, scaleCat)\n\t\t\t\t\tm[gvr] = meta\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/dao/registry_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nfunc TestMetaFor(t *testing.T) {\n\tuu := map[string]struct {\n\t\tgvr *client.GVR\n\t\terr error\n\t\te   metav1.APIResource\n\t}{\n\t\t\"xray-gvr\": {\n\t\t\tgvr: client.XGVR,\n\t\t\te: metav1.APIResource{\n\t\t\t\tName:         \"xrays\",\n\t\t\t\tKind:         \"XRays\",\n\t\t\t\tSingularName: \"xray\",\n\t\t\t\tCategories:   []string{k9sCat},\n\t\t\t},\n\t\t},\n\n\t\t\"xray\": {\n\t\t\tgvr: client.NewGVR(\"xrays\"),\n\t\t\te: metav1.APIResource{\n\t\t\t\tName:         \"xrays\",\n\t\t\t\tKind:         \"XRays\",\n\t\t\t\tSingularName: \"xray\",\n\t\t\t\tCategories:   []string{k9sCat},\n\t\t\t},\n\t\t},\n\n\t\t\"policy\": {\n\t\t\tgvr: client.NewGVR(\"policy\"),\n\t\t\te: metav1.APIResource{\n\t\t\t\tName:       \"policies\",\n\t\t\t\tKind:       \"Rules\",\n\t\t\t\tNamespaced: true,\n\t\t\t\tCategories: []string{k9sCat},\n\t\t\t},\n\t\t},\n\n\t\t\"helm\": {\n\t\t\tgvr: client.NewGVR(\"helm\"),\n\t\t\te: metav1.APIResource{\n\t\t\t\tName:       \"helm\",\n\t\t\t\tKind:       \"Helm\",\n\t\t\t\tNamespaced: true,\n\t\t\t\tVerbs:      []string{\"delete\"},\n\t\t\t\tCategories: []string{helmCat},\n\t\t\t},\n\t\t},\n\n\t\t\"toast\": {\n\t\t\tgvr: client.NewGVR(\"blah\"),\n\t\t\terr: errors.New(\"no resource meta defined for\\n \\\"blah\\\"\"),\n\t\t},\n\t}\n\n\tm := NewMeta()\n\trequire.NoError(t, m.LoadResources(nil))\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tmeta, err := m.MetaFor(u.gvr)\n\t\t\tassert.Equal(t, u.err, err)\n\t\t\tif err == nil {\n\t\t\t\tassert.Equal(t, &u.e, meta)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/dao/resource.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar (\n\t_ Accessor  = (*Resource)(nil)\n\t_ Describer = (*Resource)(nil)\n\t_ Nuker     = (*Resource)(nil)\n)\n\n// Resource represents an informer based resource.\ntype Resource struct {\n\tGeneric\n}\n\n// List returns a collection of resources.\nfunc (r *Resource) List(ctx context.Context, ns string) ([]runtime.Object, error) {\n\tlsel := labels.Everything()\n\tif sel, ok := ctx.Value(internal.KeyLabels).(labels.Selector); ok {\n\t\tlsel = sel\n\t}\n\n\treturn r.getFactory().List(r.gvr, ns, false, lsel)\n}\n\n// Get returns a resource instance if found, else an error.\nfunc (r *Resource) Get(_ context.Context, path string) (runtime.Object, error) {\n\treturn r.getFactory().Get(r.gvr, path, true, labels.Everything())\n}\n\n// ToYAML returns a resource yaml.\nfunc (r *Resource) ToYAML(path string, showManaged bool) (string, error) {\n\to, err := r.Get(context.Background(), path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\traw, err := ToYAML(o, showManaged)\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"unable to marshal resource %w\", err)\n\t}\n\treturn raw, nil\n}\n"
  },
  {
    "path": "internal/dao/rest_mapper.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"k8s.io/apimachinery/pkg/api/meta\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n\t\"k8s.io/client-go/restmapper\"\n)\n\n// RestMapping holds k8s resource mapping.\nvar RestMapping = &RestMapper{}\n\n// RestMapper map resource to REST mapping ie kind, group, version.\ntype RestMapper struct {\n\tclient.Connection\n}\n\n// ToRESTMapper map resources to kind, and map kind and version to interfaces for manipulating K8s objects.\nfunc (r *RestMapper) ToRESTMapper() (meta.RESTMapper, error) {\n\tdial, err := r.CachedDiscovery()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tmapper := restmapper.NewDeferredDiscoveryRESTMapper(dial)\n\texpander := restmapper.NewShortcutExpander(mapper, dial, nil)\n\n\treturn expander, nil\n}\n\n// ResourceFor produces a rest mapping from a given resource.\n// Support full res name ie deployment.v1.apps.\nfunc (r *RestMapper) ResourceFor(resourceArg, kind string) (*meta.RESTMapping, error) {\n\tres, err := r.resourceFor(resourceArg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn r.toRESTMapping(res, kind), nil\n}\n\nfunc (r *RestMapper) resourceFor(resourceArg string) (schema.GroupVersionResource, error) {\n\tif resourceArg == \"*\" {\n\t\treturn schema.GroupVersionResource{Resource: resourceArg}, nil\n\t}\n\n\tvar (\n\t\tgvr schema.GroupVersionResource\n\t\terr error\n\t)\n\n\tmapper, err := r.ToRESTMapper()\n\tif err != nil {\n\t\treturn gvr, err\n\t}\n\n\tfullGVR, gr := schema.ParseResourceArg(strings.ToLower(resourceArg))\n\tif fullGVR != nil {\n\t\treturn mapper.ResourceFor(*fullGVR)\n\t}\n\n\tgvr, err = mapper.ResourceFor(gr.WithVersion(\"\"))\n\tif err != nil {\n\t\tif gr.Group == \"\" {\n\t\t\treturn gvr, fmt.Errorf(\"the server doesn't have a resource type '%s'\", gr.Resource)\n\t\t}\n\t\treturn gvr, fmt.Errorf(\"the server doesn't have a resource type '%s' in group '%s'\", gr.Resource, gr.Group)\n\t}\n\n\treturn gvr, nil\n}\n\nfunc (*RestMapper) toRESTMapping(gvr schema.GroupVersionResource, kind string) *meta.RESTMapping {\n\treturn &meta.RESTMapping{\n\t\tResource: gvr,\n\t\tGroupVersionKind: schema.GroupVersionKind{\n\t\t\tGroup:   gvr.Group,\n\t\t\tVersion: gvr.Version,\n\t\t\tKind:    kind,\n\t\t},\n\t\tScope: RestMapping,\n\t}\n}\n\n// Name protocol returns rest scope name.\nfunc (*RestMapper) Name() meta.RESTScopeName {\n\treturn meta.RESTScopeNameNamespace\n}\n"
  },
  {
    "path": "internal/dao/rs.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n\tcmdutil \"k8s.io/kubectl/pkg/cmd/util\"\n\t\"k8s.io/kubectl/pkg/polymorphichelpers\"\n)\n\nvar (\n\t_ ImageLister = (*ReplicaSet)(nil)\n)\n\n// ReplicaSet represents a replicaset K8s resource.\ntype ReplicaSet struct {\n\tResource\n}\n\n// ListImages lists container images.\nfunc (r *ReplicaSet) ListImages(_ context.Context, fqn string) ([]string, error) {\n\trs, err := r.Load(r.Factory, fqn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn render.ExtractImages(&rs.Spec.Template.Spec), nil\n}\n\n// Load returns a given instance.\nfunc (*ReplicaSet) Load(f Factory, path string) (*appsv1.ReplicaSet, error) {\n\to, err := f.Get(client.RsGVR, path, true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar rs appsv1.ReplicaSet\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &rs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &rs, nil\n}\n\nfunc getRSRevision(rs *appsv1.ReplicaSet) (int64, error) {\n\trevision := rs.Annotations[\"deployment.kubernetes.io/revision\"]\n\tif rs.Status.Replicas != 0 {\n\t\treturn 0, errors.New(\"can not rollback current replica\")\n\t}\n\tvers, err := strconv.Atoi(revision)\n\tif err != nil {\n\t\treturn 0, errors.New(\"revision conversion failed\")\n\t}\n\n\treturn int64(vers), nil\n}\n\nfunc controllerInfo(rs *appsv1.ReplicaSet) (name, kind, group string, err error) {\n\tfor _, ref := range rs.OwnerReferences {\n\t\tif ref.Controller == nil {\n\t\t\tcontinue\n\t\t}\n\t\tgroup, tokens := ref.APIVersion, strings.Split(ref.APIVersion, \"/\")\n\t\tif len(tokens) == 2 {\n\t\t\tgroup = tokens[0]\n\t\t}\n\t\treturn ref.Name, ref.Kind, group, nil\n\t}\n\n\treturn \"\", \"\", \"\", fmt.Errorf(\"unable to find controller for replicaset: %s\", rs.Name)\n}\n\n// Rollback reverses the last deployment.\nfunc (r *ReplicaSet) Rollback(fqn string) error {\n\trs, err := r.Load(r.Factory, fqn)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tversion, err := getRSRevision(rs)\n\tif err != nil {\n\t\treturn err\n\t}\n\tname, kind, apiGroup, err := controllerInfo(rs)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdial, err := r.Client().Dial()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trb, err := polymorphichelpers.RollbackerFor(schema.GroupKind{\n\t\tGroup: apiGroup,\n\t\tKind:  kind,\n\t},\n\t\tdial,\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar ddp Deployment\n\tddp.Init(r.Factory, client.DpGVR)\n\tdp, err := ddp.GetInstance(client.FQN(rs.Namespace, name))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = rb.Rollback(dp, map[string]string{}, version, cmdutil.DryRunNone)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/dao/scalable.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/client-go/dynamic\"\n\t\"k8s.io/client-go/restmapper\"\n\t\"k8s.io/client-go/scale\"\n)\n\nvar (\n\t_ Scalable       = (*Scaler)(nil)\n\t_ ReplicasGetter = (*Scaler)(nil)\n)\n\n// Scaler represents a generic resource with scaling.\ntype Scaler struct {\n\tGeneric\n}\n\n// Replicas returns the number of replicas for the resource located at the given path.\nfunc (s *Scaler) Replicas(ctx context.Context, path string) (int32, error) {\n\tscaleClient, err := s.scaleClient()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tns, name := client.Namespaced(path)\n\tcurrScale, err := scaleClient.Scales(ns).Get(ctx, *s.gvr.GR(), name, metav1.GetOptions{})\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn currScale.Spec.Replicas, nil\n}\n\n// Scale modifies the number of replicas for a given resource specified by the path.\nfunc (s *Scaler) Scale(ctx context.Context, path string, replicas int32) error {\n\tns, name := client.Namespaced(path)\n\n\tscaleClient, err := s.scaleClient()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcurrentScale, err := scaleClient.Scales(ns).Get(ctx, *s.gvr.GR(), name, metav1.GetOptions{})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcurrentScale.Spec.Replicas = replicas\n\tupdatedScale, err := scaleClient.Scales(ns).Update(ctx, *s.gvr.GR(), currentScale, metav1.UpdateOptions{})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tslog.Debug(\"Scaled resource\",\n\t\tslogs.FQN, path,\n\t\tslogs.Replicas, updatedScale.Spec.Replicas,\n\t)\n\treturn nil\n}\n\nfunc (s *Scaler) scaleClient() (scale.ScalesGetter, error) {\n\tcfg, err := s.Client().RestConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdiscoveryClient, err := s.Client().CachedDiscovery()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)\n\tscaleKindResolver := scale.NewDiscoveryScaleKindResolver(discoveryClient)\n\n\treturn scale.NewForConfig(cfg, mapper, dynamic.LegacyAPIPathResolverFunc, scaleKindResolver)\n}\n"
  },
  {
    "path": "internal/dao/screen_dump.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"os\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/render\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar (\n\t_ Accessor = (*ScreenDump)(nil)\n\t_ Nuker    = (*ScreenDump)(nil)\n)\n\n// ScreenDump represents a scraped resources.\ntype ScreenDump struct {\n\tNonResource\n}\n\n// Delete a ScreenDump.\nfunc (*ScreenDump) Delete(_ context.Context, path string, _ *metav1.DeletionPropagation, _ Grace) error {\n\treturn os.Remove(path)\n}\n\n// List returns a collection of screen dumps.\nfunc (*ScreenDump) List(ctx context.Context, _ string) ([]runtime.Object, error) {\n\tdir, ok := ctx.Value(internal.KeyDir).(string)\n\tif !ok {\n\t\treturn nil, errors.New(\"no screendump dir found in context\")\n\t}\n\n\tff, err := os.ReadDir(dir)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\too := make([]runtime.Object, len(ff))\n\tfor i, f := range ff {\n\t\tif fi, err := f.Info(); err == nil {\n\t\t\too[i] = render.FileRes{File: fi, Dir: dir}\n\t\t}\n\t}\n\n\treturn oo, nil\n}\n"
  },
  {
    "path": "internal/dao/secret.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/slogs\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/cli-runtime/pkg/printers\"\n)\n\n// Secret represents a secret K8s resource.\ntype Secret struct {\n\tResource\n\tdecodeData bool\n}\n\n// Describe describes a secret that can be encoded or decoded.\nfunc (s *Secret) Describe(path string) (string, error) {\n\tencodedDescription, err := s.Generic.Describe(path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif s.decodeData {\n\t\treturn s.Decode(encodedDescription, path)\n\t}\n\n\treturn encodedDescription, nil\n}\n\n// ToYAML returns a resource yaml.\nfunc (s *Secret) ToYAML(path string, showManaged bool) (string, error) {\n\tif s.decodeData {\n\t\treturn s.decodeYAML(path, showManaged)\n\t}\n\n\treturn s.Generic.ToYAML(path, showManaged)\n}\n\nfunc (s *Secret) decodeYAML(path string, showManaged bool) (string, error) {\n\to, err := s.Get(context.Background(), path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\to = o.DeepCopyObject()\n\tu, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"expecting unstructured but got %T\", o)\n\t}\n\tif u.Object == nil {\n\t\treturn \"\", fmt.Errorf(\"expecting unstructured object but got nil\")\n\t}\n\tif !showManaged {\n\t\tif meta, ok := u.Object[\"metadata\"].(map[string]any); ok {\n\t\t\tdelete(meta, \"managedFields\")\n\t\t}\n\t}\n\tif decoded, err := ExtractSecrets(o); err == nil {\n\t\tu.Object[\"data\"] = decoded\n\t}\n\n\tvar (\n\t\tbuff bytes.Buffer\n\t\tp    printers.YAMLPrinter\n\t)\n\tif err := p.PrintObj(o, &buff); err != nil {\n\t\tslog.Error(\"PrintObj failed\", slogs.Error, err)\n\t\treturn \"\", err\n\t}\n\n\treturn buff.String(), nil\n}\n\n// SetDecodeData toggles decode mode.\nfunc (s *Secret) SetDecodeData(b bool) {\n\ts.decodeData = b\n}\n\n// Decode removes the encoded part from the secret's description and appends the\n// secret's decoded data.\nfunc (s *Secret) Decode(encodedDescription, path string) (string, error) {\n\tdataEndIndex := strings.Index(encodedDescription, \"====\")\n\tif dataEndIndex == -1 {\n\t\treturn \"\", fmt.Errorf(\"unable to find data section in secret description\")\n\t}\n\n\tdataEndIndex += 4\n\tif dataEndIndex >= len(encodedDescription) {\n\t\treturn \"\", fmt.Errorf(\"data section in secret description is invalid\")\n\t}\n\n\t// Remove the encoded part from k8s's describe API\n\t// More details about the reasoning of index: https://github.com/kubernetes/kubectl/blob/v0.29.0/pkg/describe/describe.go#L2542\n\tbody := encodedDescription[0:dataEndIndex]\n\n\to, err := s.Get(context.Background(), path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdata, err := ExtractSecrets(o)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdecodedSecrets := make([]string, 0, len(data))\n\tfor k, v := range data {\n\t\tline := fmt.Sprintf(\"%s: %s\", k, v)\n\t\tdecodedSecrets = append(decodedSecrets, strings.TrimSpace(line))\n\t}\n\n\treturn body + \"\\n\" + strings.Join(decodedSecrets, \"\\n\"), nil\n}\n\n// ExtractSecrets takes an unstructured object and attempts to convert it into a\n// Kubernetes Secret.\n// It returns a map where the keys are the secret data keys and the values are\n// the corresponding secret data values.\n// If the conversion fails, it returns an error.\nfunc ExtractSecrets(o runtime.Object) (map[string]string, error) {\n\tu, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"expecting *unstructured.Unstructured but got %T\", o)\n\t}\n\tvar secret v1.Secret\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &secret)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tsecretData := make(map[string]string, len(secret.Data))\n\tfor k, val := range secret.Data {\n\t\tsecretData[k] = string(val)\n\t}\n\n\treturn secretData, nil\n}\n"
  },
  {
    "path": "internal/dao/secret_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestEncodedSecretDescribe(t *testing.T) {\n\tvar s dao.Secret\n\ts.Init(makeFactory(), client.SecGVR)\n\n\tencodedString := `\nName: bootstrap-token-abcdef\nNamespace:    kube-system\nLabels:       <none>\nAnnotations:  <none>\n\nType:  generic\n\nData\n====\ntoken-secret:  24 bytes`\n\n\texpected := \"\\nName: bootstrap-token-abcdef\\n\" +\n\t\t\"Namespace:    kube-system\\n\" +\n\t\t\"Labels:       <none>\\n\" +\n\t\t\"Annotations:  <none>\\n\" +\n\t\t\"\\n\" +\n\t\t\"Type:  generic\\n\" +\n\t\t\"\\n\" +\n\t\t\"Data\\n\" +\n\t\t\"====\\n\" +\n\t\t\"token-secret: 0123456789abcdef\"\n\n\tdecodedDescription, err := s.Decode(encodedString, \"kube-system/bootstrap-token-abcdef\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, expected, decodedDescription)\n}\n"
  },
  {
    "path": "internal/dao/sts.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/types\"\n)\n\nvar (\n\t_ Accessor        = (*StatefulSet)(nil)\n\t_ Nuker           = (*StatefulSet)(nil)\n\t_ Loggable        = (*StatefulSet)(nil)\n\t_ Restartable     = (*StatefulSet)(nil)\n\t_ Scalable        = (*StatefulSet)(nil)\n\t_ Controller      = (*StatefulSet)(nil)\n\t_ ContainsPodSpec = (*StatefulSet)(nil)\n\t_ ImageLister     = (*StatefulSet)(nil)\n)\n\n// StatefulSet represents a K8s sts.\ntype StatefulSet struct {\n\tResource\n}\n\n// ListImages lists container images.\nfunc (s *StatefulSet) ListImages(_ context.Context, fqn string) ([]string, error) {\n\tsts, err := s.GetInstance(s.Factory, fqn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn render.ExtractImages(&sts.Spec.Template.Spec), nil\n}\n\n// Scale a StatefulSet.\nfunc (s *StatefulSet) Scale(ctx context.Context, path string, replicas int32) error {\n\treturn scaleRes(ctx, s.getFactory(), client.StsGVR, path, replicas)\n}\n\n// Restart a StatefulSet rollout.\nfunc (s *StatefulSet) Restart(ctx context.Context, path string, opts *metav1.PatchOptions) error {\n\treturn restartRes[*appsv1.StatefulSet](ctx, s.getFactory(), client.StsGVR, path, opts)\n}\n\n// GetInstance returns a statefulset instance.\nfunc (*StatefulSet) GetInstance(f Factory, fqn string) (*appsv1.StatefulSet, error) {\n\to, err := f.Get(client.StsGVR, fqn, true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar sts appsv1.StatefulSet\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &sts)\n\tif err != nil {\n\t\treturn nil, errors.New(\"expecting Statefulset resource\")\n\t}\n\n\treturn &sts, nil\n}\n\n// TailLogs tail logs for all pods represented by this StatefulSet.\nfunc (s *StatefulSet) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) {\n\tsts, err := s.getStatefulSet(opts.Path)\n\tif err != nil {\n\t\treturn nil, errors.New(\"expecting StatefulSet resource\")\n\t}\n\tif sts.Spec.Selector == nil || len(sts.Spec.Selector.MatchLabels) == 0 {\n\t\treturn nil, fmt.Errorf(\"no valid selector found on statefulset: %s\", opts.Path)\n\t}\n\n\treturn podLogs(ctx, sts.Spec.Selector.MatchLabels, opts)\n}\n\n// Pod returns a pod victim by name.\nfunc (s *StatefulSet) Pod(fqn string) (string, error) {\n\tsts, err := s.getStatefulSet(fqn)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn podFromSelector(s.Factory, sts.Namespace, sts.Spec.Selector.MatchLabels)\n}\n\nfunc (s *StatefulSet) getStatefulSet(fqn string) (*appsv1.StatefulSet, error) {\n\to, err := s.getFactory().Get(s.gvr, fqn, true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar sts appsv1.StatefulSet\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &sts)\n\tif err != nil {\n\t\treturn nil, errors.New(\"expecting Service resource\")\n\t}\n\n\treturn &sts, nil\n}\n\n// ScanSA scans for serviceaccount refs.\nfunc (s *StatefulSet) ScanSA(_ context.Context, fqn string, wait bool) (Refs, error) {\n\tns, n := client.Namespaced(fqn)\n\too, err := s.getFactory().List(s.gvr, ns, wait, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trefs := make(Refs, 0, len(oo))\n\tfor _, o := range oo {\n\t\tvar sts appsv1.StatefulSet\n\t\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &sts)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(\"expecting StatefulSet resource\")\n\t\t}\n\t\tif serviceAccountMatches(sts.Spec.Template.Spec.ServiceAccountName, n) {\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: s.GVR(),\n\t\t\t\tFQN: client.FQN(sts.Namespace, sts.Name),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn refs, nil\n}\n\n// Scan scans for cluster resource refs.\nfunc (s *StatefulSet) Scan(_ context.Context, gvr *client.GVR, fqn string, wait bool) (Refs, error) {\n\tns, n := client.Namespaced(fqn)\n\too, err := s.getFactory().List(s.gvr, ns, wait, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trefs := make(Refs, 0, len(oo))\n\tfor _, o := range oo {\n\t\tvar sts appsv1.StatefulSet\n\t\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &sts)\n\t\tif err != nil {\n\t\t\treturn nil, errors.New(\"expecting StatefulSet resource\")\n\t\t}\n\t\tswitch gvr {\n\t\tcase client.CmGVR:\n\t\t\tif !hasConfigMap(&sts.Spec.Template.Spec, n) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: s.GVR(),\n\t\t\t\tFQN: client.FQN(sts.Namespace, sts.Name),\n\t\t\t})\n\t\tcase client.SecGVR:\n\t\t\tfound, err := hasSecret(s.Factory, &sts.Spec.Template.Spec, sts.Namespace, n, wait)\n\t\t\tif err != nil {\n\t\t\t\tslog.Warn(\"Locate secret failed\",\n\t\t\t\t\tslogs.FQN, fqn,\n\t\t\t\t\tslogs.Error, err,\n\t\t\t\t)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif !found {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: s.GVR(),\n\t\t\t\tFQN: client.FQN(sts.Namespace, sts.Name),\n\t\t\t})\n\t\tcase client.PvcGVR:\n\t\t\tfor i := range sts.Spec.VolumeClaimTemplates {\n\t\t\t\tif !strings.HasPrefix(n, sts.Spec.VolumeClaimTemplates[i].Name+\"-\"+sts.Name) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\trefs = append(refs, Ref{\n\t\t\t\t\tGVR: s.GVR(),\n\t\t\t\t\tFQN: client.FQN(sts.Namespace, sts.Name),\n\t\t\t\t})\n\t\t\t}\n\t\t\tif !hasPVC(&sts.Spec.Template.Spec, n) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: s.GVR(),\n\t\t\t\tFQN: client.FQN(sts.Namespace, sts.Name),\n\t\t\t})\n\t\tcase client.PcGVR:\n\t\t\tif !hasPC(&sts.Spec.Template.Spec, n) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\trefs = append(refs, Ref{\n\t\t\t\tGVR: s.GVR(),\n\t\t\t\tFQN: client.FQN(sts.Namespace, sts.Name),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn refs, nil\n}\n\n// GetPodSpec returns a pod spec given a resource.\nfunc (s *StatefulSet) GetPodSpec(path string) (*v1.PodSpec, error) {\n\tsts, err := s.getStatefulSet(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tpodSpec := sts.Spec.Template.Spec\n\treturn &podSpec, nil\n}\n\n// SetImages sets container images.\nfunc (s *StatefulSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error {\n\tns, n := client.Namespaced(path)\n\tauth, err := s.Client().CanI(ns, client.StsGVR, n, client.PatchAccess)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !auth {\n\t\treturn fmt.Errorf(\"user is not authorized to patch a statefulset\")\n\t}\n\tjsonPatch, err := GetTemplateJsonPatch(imageSpecs)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdial, err := s.Client().Dial()\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = dial.AppsV1().StatefulSets(ns).Patch(\n\t\tctx,\n\t\tn,\n\t\ttypes.StrategicMergePatchType,\n\t\tjsonPatch,\n\t\tmetav1.PatchOptions{},\n\t)\n\treturn err\n}\n"
  },
  {
    "path": "internal/dao/svc.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar (\n\t_ Accessor   = (*Service)(nil)\n\t_ Loggable   = (*Service)(nil)\n\t_ Controller = (*Service)(nil)\n)\n\n// Service represents a k8s service.\ntype Service struct {\n\tResource\n}\n\n// TailLogs tail logs for all pods represented by this Service.\nfunc (s *Service) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) {\n\tsvc, err := s.GetInstance(opts.Path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(svc.Spec.Selector) == 0 {\n\t\treturn nil, fmt.Errorf(\"no valid selector found on Service %s\", opts.Path)\n\t}\n\n\treturn podLogs(ctx, svc.Spec.Selector, opts)\n}\n\n// Pod returns a pod victim by name.\nfunc (s *Service) Pod(fqn string) (string, error) {\n\tsvc, err := s.GetInstance(fqn)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn podFromSelector(s.Factory, svc.Namespace, svc.Spec.Selector)\n}\n\n// GetInstance returns a service instance.\nfunc (s *Service) GetInstance(fqn string) (*v1.Service, error) {\n\to, err := s.getFactory().Get(s.gvr, fqn, true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar svc v1.Service\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &svc)\n\tif err != nil {\n\t\treturn nil, errors.New(\"expecting Service resource\")\n\t}\n\n\treturn &svc, nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc podFromSelector(f Factory, ns string, sel map[string]string) (string, error) {\n\too, err := f.List(client.PodGVR, ns, true, labels.Set(sel).AsSelector())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif len(oo) == 0 {\n\t\treturn \"\", fmt.Errorf(\"no matching pods for %v\", sel)\n\t}\n\n\tvar pod v1.Pod\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(oo[0].(*unstructured.Unstructured).Object, &pod)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn client.FQN(pod.Namespace, pod.Name), nil\n}\n"
  },
  {
    "path": "internal/dao/table.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"k8s.io/apimachinery/pkg/api/meta\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/serializer\"\n\t\"k8s.io/client-go/rest\"\n)\n\nconst (\n\tgvFmt       = \"application/json;as=Table;v=%s;g=%s, application/json\"\n\tincludeMeta = \"Metadata\"\n\tincludeObj  = \"Object\"\n\tincludeNone = \"None\"\n\theader      = \"application/json;as=Table;v=v1;g=meta.k8s.io,application/json;as=Table;v=v1beta1;g=meta.k8s.io,application/json\"\n)\n\nvar genScheme = runtime.NewScheme()\n\n// Table retrieves K8s resources as tabular data.\ntype Table struct {\n\tGeneric\n}\n\n// Get returns a given resource.\nfunc (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) {\n\tf, p := t.codec()\n\tc, err := t.getClient(f)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tns, n := client.Namespaced(path)\n\ta := fmt.Sprintf(gvFmt, metav1.SchemeGroupVersion.Version, metav1.GroupName)\n\treq := c.Get().\n\t\tSetHeader(\"Accept\", a).\n\t\tName(n).\n\t\tResource(t.gvr.R()).\n\t\tVersionedParams(&metav1.TableOptions{}, p)\n\tif ns != client.ClusterScope {\n\t\treq = req.Namespace(ns)\n\t}\n\n\treturn req.Do(ctx).Get()\n}\n\n// List all Resources in a given namespace.\nfunc (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) {\n\tsel := labels.Everything()\n\tif labelSel, ok := ctx.Value(internal.KeyLabels).(labels.Selector); ok {\n\t\tsel = labelSel\n\t}\n\tfieldSel, _ := ctx.Value(internal.KeyFields).(string)\n\n\tincludeObject := includeMeta\n\tif t.includeObj {\n\t\tincludeObject = includeObj\n\t}\n\n\tf, _ := t.codec()\n\tc, err := t.getClient(f)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\to, err := c.Get().\n\t\tSetHeader(\"Accept\", header).\n\t\tParam(\"includeObject\", includeObject).\n\t\tNamespace(ns).\n\t\tResource(t.gvr.R()).\n\t\tVersionedParams(&metav1.ListOptions{\n\t\t\tLabelSelector: sel.String(),\n\t\t\tFieldSelector: fieldSel,\n\t\t}, metav1.ParameterCodec).\n\t\tDo(ctx).Get()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnamespaced := true\n\tif res, e := MetaAccess.MetaFor(t.gvr); e == nil && !res.Namespaced {\n\t\tnamespaced = false\n\t}\n\tta, err := decodeTable(ctx, o.(*metav1.Table), namespaced)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn []runtime.Object{ta}, nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc decodeTable(ctx context.Context, table *metav1.Table, namespaced bool) (runtime.Object, error) {\n\tif namespaced {\n\t\ttable.ColumnDefinitions = append([]metav1.TableColumnDefinition{{Name: \"Namespace\", Type: \"string\"}}, table.ColumnDefinitions...)\n\t}\n\tpool := internal.NewWorkerPool(ctx, internal.DefaultPoolSize)\n\tfor i := range table.Rows {\n\t\tpool.Add(func(_ context.Context) error {\n\t\t\trow := &table.Rows[i]\n\t\t\tif row.Object.Raw == nil || row.Object.Object != nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tconverted, err := runtime.Decode(unstructured.UnstructuredJSONScheme, row.Object.Raw)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\trow.Object.Object = converted\n\t\t\tvar m metav1.Object\n\t\t\tif obj := row.Object.Object; obj != nil {\n\t\t\t\tm, _ = meta.Accessor(obj)\n\t\t\t}\n\t\t\tvar ns string\n\t\t\tif m != nil {\n\t\t\t\tns = m.GetNamespace()\n\t\t\t}\n\t\t\tif namespaced {\n\t\t\t\trow.Cells = append([]any{ns}, row.Cells...)\n\t\t\t}\n\t\t\treturn nil\n\t\t})\n\t}\n\terrs := pool.Drain()\n\tif len(errs) > 0 {\n\t\treturn nil, fmt.Errorf(\"failed to decode table rows: %w\", errs[0])\n\t}\n\n\treturn table, nil\n}\n\nfunc (t *Table) getClient(f serializer.CodecFactory) (*rest.RESTClient, error) {\n\tcfg, err := t.Client().RestConfig()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tgv := t.gvr.GV()\n\tcfg.GroupVersion = &gv\n\tcfg.APIPath = \"/apis\"\n\tif t.gvr.G() == \"\" {\n\t\tcfg.APIPath = \"/api\"\n\t}\n\tcfg.NegotiatedSerializer = f.WithoutConversion()\n\tcrRestClient, err := rest.RESTClientFor(cfg)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn crRestClient, nil\n}\n\nfunc (t *Table) codec() (serializer.CodecFactory, runtime.ParameterCodec) {\n\tvar tt metav1.Table\n\topts := metav1.TableOptions{IncludeObject: metav1.IncludeObject}\n\tgv := t.gvr.GV()\n\tmetav1.AddToGroupVersion(genScheme, gv)\n\tgenScheme.AddKnownTypes(gv, &tt, &opts)\n\tgenScheme.AddKnownTypes(metav1.SchemeGroupVersion, &tt, &opts)\n\n\treturn serializer.NewCodecFactory(genScheme), runtime.NewParameterCodec(genScheme)\n}\n"
  },
  {
    "path": "internal/dao/testdata/bench/default_fred_1577308050814961000.txt",
    "content": "Summary:\n  Total:\t816.6403 secs\n  Slowest:\t0.0000 secs\n  Fastest:\t0.0000 secs\n  Average:\t NaN secs\n  Requests/sec:\t0.0122\n\n\nResponse time histogram:\n\n\nLatency distribution:\n\nDetails (average, fastest, slowest):\n  DNS+dialup:\t NaN secs, 0.0000 secs, 0.0000 secs\n  DNS-lookup:\t NaN secs, 0.0000 secs, 0.0000 secs\n  req write:\t NaN secs, 0.0000 secs, 0.0000 secs\n  resp wait:\t NaN secs, 0.0000 secs, 0.0000 secs\n  resp read:\t NaN secs, 0.0000 secs, 0.0000 secs\n\nStatus code distribution:\n\nError distribution:\n  [10]\tGet http://192.168.64.126:30805/: dial tcp 192.168.64.126:30805: connect: operation timed out\n"
  },
  {
    "path": "internal/dao/testdata/benchspec.yaml",
    "content": "benchmarks:\n  defaults:\n    concurrency: 2\n    requests: 500\n  containers:\n    default/nginx:nginx:\n      concurrency: 2\n      requests: 3000\n      http:\n        method: GET\n        path: /\n  services:\n    default/nginx:\n      concurrency: 1\n      requests: 666\n      http:\n        method: GET\n        host: 192.168.64.1\n        path: /\n"
  },
  {
    "path": "internal/dao/testdata/config",
    "content": "apiVersion: v1\nkind: Config\npreferences: {}\nclusters:\n- cluster:\n    insecure-skip-tls-verify: true\n    server: https://localhost:3000\n  name: fred\n- cluster:\n    insecure-skip-tls-verify: true\n    server: https://localhost:3001\n  name: blee\n- cluster:\n    insecure-skip-tls-verify: true\n    server: https://localhost:3002\n  name: duh\ncontexts:\n- context:\n    cluster: fred\n    user: fred\n  name: fred\n- context:\n    cluster: blee\n    namespace: zorg\n    user: blee\n  name: blee\n- context:\n    cluster: duh\n    user: duh\n  name: duh\ncurrent-context: fred\nusers:\n- name: fred\n  user:\n    client-certificate-data: ZnJlZA==\n    client-key-data: ZnJlZA==\n- name: blee\n  user:\n    client-certificate-data: ZnJlZA==\n    client-key-data: ZnJlZA==\n- name: duh\n  user:\n    client-certificate-data: ZnJlZA==\n    client-key-data: ZnJlZA==\n"
  },
  {
    "path": "internal/dao/testdata/config.1",
    "content": "apiVersion: v1\nclusters:\n- cluster:\n    insecure-skip-tls-verify: true\n    server: https://localhost:3001\n  name: blee\n- cluster:\n    insecure-skip-tls-verify: true\n    server: https://localhost:3002\n  name: duh\n- cluster:\n    insecure-skip-tls-verify: true\n    server: https://localhost:3000\n  name: fred\ncontexts:\n- context:\n    cluster: blee\n    user: blee\n  name: blee\n- context:\n    cluster: duh\n    user: duh\n  name: duh\ncurrent-context: fred\nkind: Config\npreferences: {}\nusers:\n- name: blee\n  user:\n    client-certificate-data: ZnJlZA==\n    client-key-data: ZnJlZA==\n- name: duh\n  user:\n    client-certificate-data: ZnJlZA==\n    client-key-data: ZnJlZA==\n- name: fred\n  user:\n    client-certificate-data: ZnJlZA==\n    client-key-data: ZnJlZA==\n"
  },
  {
    "path": "internal/dao/testdata/crb.json",
    "content": "{\n  \"apiVersion\": \"rbac.authorization.k8s.io/v1\",\n  \"kind\": \"ClusterRoleBinding\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"rbac.authorization.k8s.io/v1\\\",\\\"kind\\\":\\\"ClusterRoleBinding\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"blee\\\"},\\\"roleRef\\\":{\\\"apiGroup\\\":\\\"rbac.authorization.k8s.io\\\",\\\"kind\\\":\\\"ClusterRole\\\",\\\"name\\\":\\\"blee\\\"},\\\"subjects\\\":[{\\\"apiGroup\\\":\\\"rbac.authorization.k8s.io\\\",\\\"kind\\\":\\\"User\\\",\\\"name\\\":\\\"fernand\\\"}]}\\n\"\n    },\n    \"creationTimestamp\": \"2019-06-04T16:48:35Z\",\n    \"name\": \"blee\",\n    \"resourceVersion\": \"26689100\",\n    \"selfLink\": \"/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/blee\",\n    \"uid\": \"97e5f84d-86e8-11e9-a8e8-42010a80015b\"\n  },\n  \"roleRef\": {\n    \"apiGroup\": \"rbac.authorization.k8s.io\",\n    \"kind\": \"ClusterRole\",\n    \"name\": \"blee\"\n  },\n  \"subjects\": [\n    {\n      \"apiGroup\": \"rbac.authorization.k8s.io\",\n      \"kind\": \"User\",\n      \"name\": \"fernand\"\n    }\n  ]\n}"
  },
  {
    "path": "internal/dao/testdata/dir/a/b.yaml",
    "content": ""
  },
  {
    "path": "internal/dao/testdata/dir/a.yaml",
    "content": ""
  },
  {
    "path": "internal/dao/testdata/dr.json",
    "content": "{\n  \"apiVersion\": \"apiextensions.k8s.io/v1\",\n  \"kind\": \"CustomResourceDefinition\",\n  \"metadata\": {\n    \"annotations\": {\n      \"helm.sh/resource-policy\": \"keep\",\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"apiextensions.k8s.io/v1\\\",\\\"kind\\\":\\\"CustomResourceDefinition\\\",\\\"metadata\\\":{\\\"annotations\\\":{\\\"helm.sh/resource-policy\\\":\\\"keep\\\"},\\\"labels\\\":{\\\"app\\\":\\\"istio-pilot\\\",\\\"chart\\\":\\\"istio\\\",\\\"heritage\\\":\\\"Tiller\\\",\\\"release\\\":\\\"istio\\\"},\\\"name\\\":\\\"destinationrules.networking.istio.io\\\"},\\\"spec\\\":{\\\"additionalPrinterColumns\\\":[{\\\"JSONPath\\\":\\\".spec.host\\\",\\\"description\\\":\\\"The name of a service from the service registry\\\",\\\"name\\\":\\\"Host\\\",\\\"type\\\":\\\"string\\\"},{\\\"JSONPath\\\":\\\".metadata.creationTimestamp\\\",\\\"description\\\":\\\"CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\\\\n\\\\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata\\\",\\\"name\\\":\\\"Age\\\",\\\"type\\\":\\\"date\\\"}],\\\"group\\\":\\\"networking.istio.io\\\",\\\"names\\\":{\\\"categories\\\":[\\\"istio-io\\\",\\\"networking-istio-io\\\"],\\\"kind\\\":\\\"DestinationRule\\\",\\\"listKind\\\":\\\"DestinationRuleList\\\",\\\"plural\\\":\\\"destinationrules\\\",\\\"shortNames\\\":[\\\"dr\\\"],\\\"singular\\\":\\\"destinationrule\\\"},\\\"scope\\\":\\\"Namespaced\\\",\\\"version\\\":\\\"v1alpha3\\\"}}\\n\"\n    },\n    \"creationTimestamp\": \"2019-12-30T16:13:02Z\",\n    \"generation\": 1,\n    \"labels\": {\n      \"app\": \"istio-pilot\",\n      \"chart\": \"istio\",\n      \"heritage\": \"Tiller\",\n      \"release\": \"istio\"\n    },\n    \"name\": \"destinationrules.networking.istio.io\",\n    \"resourceVersion\": \"2773373\",\n    \"selfLink\": \"/apis/apiextensions.k8s.io/v1/customresourcedefinitions/destinationrules.networking.istio.io\",\n    \"uid\": \"123a30f8-8fcf-44b5-84b7-35f8c7869828\"\n  },\n  \"spec\": {\n    \"conversion\": {\n      \"strategy\": \"None\"\n    },\n    \"group\": \"networking.istio.io\",\n    \"version\": \"v1alpha3\",\n    \"names\": {\n      \"categories\": [\n        \"istio-io\",\n        \"networking-istio-io\"\n      ],\n      \"kind\": \"DestinationRule\",\n      \"listKind\": \"DestinationRuleList\",\n      \"plural\": \"destinationrules\",\n      \"shortNames\": [\n        \"dr\"\n      ],\n      \"singular\": \"destinationrule\"\n    },\n    \"preserveUnknownFields\": true,\n    \"scope\": \"Namespaced\",\n    \"versions\": [\n      {\n        \"additionalPrinterColumns\": [\n          {\n            \"description\": \"The name of a service from the service registry\",\n            \"jsonPath\": \".spec.host\",\n            \"name\": \"Host\",\n            \"type\": \"string\"\n          },\n          {\n            \"description\": \"CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\\n\\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata\",\n            \"jsonPath\": \".metadata.creationTimestamp\",\n            \"name\": \"Age\",\n            \"type\": \"date\"\n          }\n        ],\n        \"name\": \"v1alpha3\",\n        \"served\": true,\n        \"storage\": true\n      }\n    ]\n  },\n  \"status\": {\n    \"acceptedNames\": {\n      \"categories\": [\n        \"istio-io\",\n        \"networking-istio-io\"\n      ],\n      \"kind\": \"DestinationRule\",\n      \"listKind\": \"DestinationRuleList\",\n      \"plural\": \"destinationrules\",\n      \"shortNames\": [\n        \"dr\"\n      ],\n      \"singular\": \"destinationrule\"\n    },\n    \"conditions\": [\n      {\n        \"lastTransitionTime\": \"2019-12-30T16:13:02Z\",\n        \"message\": \"no conflicts found\",\n        \"reason\": \"NoConflicts\",\n        \"status\": \"True\",\n        \"type\": \"NamesAccepted\"\n      },\n      {\n        \"lastTransitionTime\": \"2019-12-30T16:13:02Z\",\n        \"message\": \"the initial names have been accepted\",\n        \"reason\": \"InitialNamesAccepted\",\n        \"status\": \"True\",\n        \"type\": \"Established\"\n      }\n    ],\n    \"storedVersions\": [\n      \"v1alpha3\"\n    ]\n  }\n}"
  },
  {
    "path": "internal/dao/testdata/n1.json",
    "content": "{\n  \"apiVersion\": \"v1\",\n  \"kind\": \"Node\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubeadm.alpha.kubernetes.io/cri-socket\": \"/var/run/dockershim.sock\",\n      \"node.alpha.kubernetes.io/ttl\": \"0\",\n      \"volumes.kubernetes.io/controller-managed-attach-detach\": \"true\"\n    },\n    \"creationTimestamp\": \"2019-12-31T20:49:21Z\",\n    \"labels\": {\n      \"beta.kubernetes.io/arch\": \"amd64\",\n      \"beta.kubernetes.io/os\": \"linux\",\n      \"kubernetes.io/arch\": \"amd64\",\n      \"kubernetes.io/hostname\": \"minikube\",\n      \"kubernetes.io/os\": \"linux\",\n      \"node-role.kubernetes.io/master\": \"\"\n    },\n    \"name\": \"minikube\",\n    \"resourceVersion\": \"214450\",\n    \"selfLink\": \"/api/v1/nodes/minikube\",\n    \"uid\": \"a33a26f0-7688-47b6-8dbf-5a04ea7f43d4\"\n  },\n  \"spec\": {},\n  \"status\": {\n    \"addresses\": [\n      {\n        \"address\": \"192.168.64.6\",\n        \"type\": \"InternalIP\"\n      },\n      {\n        \"address\": \"minikube\",\n        \"type\": \"Hostname\"\n      }\n    ],\n    \"allocatable\": {\n      \"cpu\": \"4\",\n      \"ephemeral-storage\": \"16954240Ki\",\n      \"hugepages-2Mi\": \"0\",\n      \"memory\": \"8163684Ki\",\n      \"pods\": \"110\"\n    },\n    \"capacity\": {\n      \"cpu\": \"4\",\n      \"ephemeral-storage\": \"16954240Ki\",\n      \"hugepages-2Mi\": \"0\",\n      \"memory\": \"8163684Ki\",\n      \"pods\": \"110\"\n    },\n    \"conditions\": [\n      {\n        \"lastHeartbeatTime\": \"2020-01-01T22:05:55Z\",\n        \"lastTransitionTime\": \"2019-12-31T20:49:18Z\",\n        \"message\": \"kubelet has sufficient memory available\",\n        \"reason\": \"KubeletHasSufficientMemory\",\n        \"status\": \"False\",\n        \"type\": \"MemoryPressure\"\n      },\n      {\n        \"lastHeartbeatTime\": \"2020-01-01T22:05:55Z\",\n        \"lastTransitionTime\": \"2019-12-31T20:49:18Z\",\n        \"message\": \"kubelet has no disk pressure\",\n        \"reason\": \"KubeletHasNoDiskPressure\",\n        \"status\": \"False\",\n        \"type\": \"DiskPressure\"\n      },\n      {\n        \"lastHeartbeatTime\": \"2020-01-01T22:05:55Z\",\n        \"lastTransitionTime\": \"2019-12-31T20:49:18Z\",\n        \"message\": \"kubelet has sufficient PID available\",\n        \"reason\": \"KubeletHasSufficientPID\",\n        \"status\": \"False\",\n        \"type\": \"PIDPressure\"\n      },\n      {\n        \"lastHeartbeatTime\": \"2020-01-01T22:05:55Z\",\n        \"lastTransitionTime\": \"2019-12-31T20:49:22Z\",\n        \"message\": \"kubelet is posting ready status\",\n        \"reason\": \"KubeletReady\",\n        \"status\": \"True\",\n        \"type\": \"Ready\"\n      }\n    ],\n    \"daemonEndpoints\": {\n      \"kubeletEndpoint\": {\n        \"Port\": 10250\n      }\n    },\n    \"images\": [\n      {\n        \"names\": [\n          \"quay.io/kubernetes-ingress-controller/nginx-ingress-controller@sha256:d0b22f715fcea5598ef7f869d308b55289a3daaa12922fa52a1abf17703c88e7\",\n          \"quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.26.1\"\n        ],\n        \"sizeBytes\": 483167446\n      },\n      {\n        \"names\": [\n          \"istio/proxyv2@sha256:236527816ff67f8492d7286775e09c28e207aee2f6f3c3d9258cd2248af4afa5\",\n          \"istio/proxyv2:1.2.2\"\n        ],\n        \"sizeBytes\": 369614978\n      },\n      {\n        \"names\": [\n          \"quay.io/kiali/kiali@sha256:60ceb57682e95fa3fb7c6e12d797f21c9e242c5583fa024a859d1085d0985c7b\",\n          \"quay.io/kiali/kiali:v0.20\"\n        ],\n        \"sizeBytes\": 344083595\n      },\n      {\n        \"names\": [\n          \"istio/kubectl@sha256:a94f8f992bc1e996319a58ff934f9c5e6658e2338fb59e1d937f919b8146d050\",\n          \"istio/kubectl:1.2.2\"\n        ],\n        \"sizeBytes\": 341145787\n      },\n      {\n        \"names\": [\n          \"istio/galley@sha256:786bb02b6d425697826ce740d723664beababf7a513eb8d4c95b42b35a99e91d\",\n          \"istio/galley:1.2.2\"\n        ],\n        \"sizeBytes\": 306543175\n      },\n      {\n        \"names\": [\n          \"istio/pilot@sha256:ab08845a7f4d1fd44c8481b35161a8da0cbf880f3d4f690740aec27350758a95\",\n          \"istio/pilot:1.2.2\"\n        ],\n        \"sizeBytes\": 303914365\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/etcd@sha256:4afb99b4690b418ffc2ceb67e1a17376457e441c1f09ab55447f0aaf992fa646\",\n          \"k8s.gcr.io/etcd:3.4.3-0\"\n        ],\n        \"sizeBytes\": 288426917\n      },\n      {\n        \"names\": [\n          \"grafana/grafana@sha256:d66b41cf7e0586274ca3e15e03299e4cfde48019fd756bb97cc9db57da9b0c86\",\n          \"grafana/grafana:6.1.6\"\n        ],\n        \"sizeBytes\": 245005426\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/kube-apiserver@sha256:e3ec33d533257902ad9ebe3d399c17710e62009201a7202aec941e351545d662\",\n          \"k8s.gcr.io/kube-apiserver:v1.17.0\"\n        ],\n        \"sizeBytes\": 170957331\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/kube-controller-manager@sha256:0438efb5098a2ca634ea8c6b0d804742b733d0d13fd53cf62c73e32c659a3c39\",\n          \"k8s.gcr.io/kube-controller-manager:v1.17.0\"\n        ],\n        \"sizeBytes\": 160877075\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/kube-proxy@sha256:b2ba9441af30261465e5c41be63e462d0050b09ad280001ae731f399b2b00b75\",\n          \"k8s.gcr.io/kube-proxy:v1.17.0\"\n        ],\n        \"sizeBytes\": 115960823\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/nginx-slim@sha256:8b4501fe0fe221df663c22e16539f399e89594552f400408303c42f3dd8d0e52\",\n          \"k8s.gcr.io/nginx-slim:0.8\"\n        ],\n        \"sizeBytes\": 110487599\n      },\n      {\n        \"names\": [\n          \"prom/prometheus@sha256:1224ee30a3be668e0b22444773c4c1b750778af492094b6cd375c780c7526e22\",\n          \"prom/prometheus:v2.8.0\"\n        ],\n        \"sizeBytes\": 108629897\n      },\n      {\n        \"names\": [\n          \"istio/mixer@sha256:886726967363477eeba4cbf48675b058bcf833c932763b0964db80390fc06ceb\",\n          \"istio/mixer:1.2.2\"\n        ],\n        \"sizeBytes\": 97783922\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/kube-scheduler@sha256:5215c4216a65f7e76c1895ba951a12dc1c947904a91810fc66a544ff1d7e87db\",\n          \"k8s.gcr.io/kube-scheduler:v1.17.0\"\n        ],\n        \"sizeBytes\": 94431763\n      },\n      {\n        \"names\": [\n          \"kubernetesui/dashboard:v2.0.0-beta8\"\n        ],\n        \"sizeBytes\": 90835427\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/kube-addon-manager:v9.0.2\"\n        ],\n        \"sizeBytes\": 83076028\n      },\n      {\n        \"names\": [\n          \"gcr.io/k8s-minikube/storage-provisioner:v1.8.1\"\n        ],\n        \"sizeBytes\": 80815640\n      },\n      {\n        \"names\": [\n          \"istio/citadel@sha256:1e8065b277cb79a32ef617f7af468f9afe5b21ec2e0b42245d029c59fe3ce435\",\n          \"istio/citadel:1.2.2\"\n        ],\n        \"sizeBytes\": 68454561\n      },\n      {\n        \"names\": [\n          \"istio/sidecar_injector@sha256:c8f6f5fb1bb2434f68199e06b124e85dc58a3879bf1275a4d39c400836bd3ca4\",\n          \"istio/sidecar_injector:1.2.2\"\n        ],\n        \"sizeBytes\": 63917960\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/metrics-server-amd64@sha256:49a9f12f7067d11f42c803dbe61ed2c1299959ad85cb315b25ff7eef8e6b8892\",\n          \"k8s.gcr.io/metrics-server-amd64:v0.2.1\"\n        ],\n        \"sizeBytes\": 42541759\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/coredns@sha256:7ec975f167d815311a7136c32e70735f0d00b73781365df1befd46ed35bd4fe7\",\n          \"k8s.gcr.io/coredns:1.6.5\"\n        ],\n        \"sizeBytes\": 41578211\n      },\n      {\n        \"names\": [\n          \"kubernetesui/metrics-scraper:v1.0.2\"\n        ],\n        \"sizeBytes\": 40101552\n      },\n      {\n        \"names\": [\n          \"jaegertracing/all-in-one@sha256:29c921747eddfa96c97cf96aac0180e97bfdfcbea25e230daef09711103d1f61\",\n          \"jaegertracing/all-in-one:1.9\"\n        ],\n        \"sizeBytes\": 37328894\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/pause@sha256:f78411e19d84a252e53bff71a4407a5686c46983a2c2eeed83929b888179acea\",\n          \"k8s.gcr.io/pause:3.1\"\n        ],\n        \"sizeBytes\": 742472\n      }\n    ],\n    \"nodeInfo\": {\n      \"architecture\": \"amd64\",\n      \"bootID\": \"478c895b-009b-4b6e-9115-63502eaa68cb\",\n      \"containerRuntimeVersion\": \"docker://19.3.5\",\n      \"kernelVersion\": \"4.19.81\",\n      \"kubeProxyVersion\": \"v1.17.0\",\n      \"kubeletVersion\": \"v1.17.0\",\n      \"machineID\": \"6c484e2bfebf46f2ac854c484bcfa392\",\n      \"operatingSystem\": \"linux\",\n      \"osImage\": \"Buildroot 2019.02.7\",\n      \"systemUUID\": \"dbc511ea-0000-0000-a42f-acde48001122\"\n    }\n  }\n}"
  },
  {
    "path": "internal/dao/testdata/p1.json",
    "content": "{\n  \"apiVersion\": \"v1\",\n  \"kind\": \"Pod\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/restartedAt\": \"2019-12-31T12:26:47-07:00\"\n    },\n    \"creationTimestamp\": \"2019-12-31T19:27:22Z\",\n    \"generateName\": \"nginx-7fb78fb6d8-\",\n    \"labels\": {\n      \"app\": \"nginx\",\n      \"pod-template-hash\": \"7fb78fb6d8\"\n    },\n    \"name\": \"nginx-7fb78fb6d8-2w75j\",\n    \"namespace\": \"default\",\n    \"ownerReferences\": [\n      {\n        \"apiVersion\": \"apps/v1\",\n        \"blockOwnerDeletion\": true,\n        \"controller\": true,\n        \"kind\": \"ReplicaSet\",\n        \"name\": \"nginx-7fb78fb6d8\",\n        \"uid\": \"7ccd0600-2c03-11ea-883f-42010a800044\"\n      }\n    ],\n    \"resourceVersion\": \"87290191\",\n    \"selfLink\": \"/api/v1/namespaces/default/pods/nginx-7fb78fb6d8-2w75j\",\n    \"uid\": \"91bb1cf2-2c03-11ea-883f-42010a800044\"\n  },\n  \"spec\": {\n    \"containers\": [\n      {\n        \"image\": \"k8s.gcr.io/nginx-slim:0.8\",\n        \"imagePullPolicy\": \"IfNotPresent\",\n        \"name\": \"nginx\",\n        \"ports\": [\n          {\n            \"containerPort\": 80,\n            \"protocol\": \"TCP\"\n          }\n        ],\n        \"resources\": {\n          \"limits\": {\n            \"cpu\": \"200m\",\n            \"memory\": \"20Mi\"\n          },\n          \"requests\": {\n            \"cpu\": \"200m\",\n            \"memory\": \"20Mi\"\n          }\n        },\n        \"terminationMessagePath\": \"/dev/termination-log\",\n        \"terminationMessagePolicy\": \"File\",\n        \"volumeMounts\": [\n          {\n            \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\",\n            \"name\": \"default-token-dsl46\",\n            \"readOnly\": true\n          }\n        ]\n      }\n    ],\n    \"dnsPolicy\": \"ClusterFirst\",\n    \"enableServiceLinks\": true,\n    \"nodeName\": \"gke-k9s-default-pool-0fa2fb89-lbtf\",\n    \"priority\": 0,\n    \"restartPolicy\": \"Always\",\n    \"schedulerName\": \"default-scheduler\",\n    \"securityContext\": {},\n    \"serviceAccount\": \"default\",\n    \"serviceAccountName\": \"default\",\n    \"terminationGracePeriodSeconds\": 30,\n    \"tolerations\": [\n      {\n        \"effect\": \"NoExecute\",\n        \"key\": \"node.kubernetes.io/not-ready\",\n        \"operator\": \"Exists\",\n        \"tolerationSeconds\": 300\n      },\n      {\n        \"effect\": \"NoExecute\",\n        \"key\": \"node.kubernetes.io/unreachable\",\n        \"operator\": \"Exists\",\n        \"tolerationSeconds\": 300\n      }\n    ],\n    \"volumes\": [\n      {\n        \"name\": \"default-token-dsl46\",\n        \"secret\": {\n          \"defaultMode\": 420,\n          \"secretName\": \"default-token-dsl46\"\n        }\n      }\n    ]\n  },\n  \"status\": {\n    \"conditions\": [\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-12-31T19:27:23Z\",\n        \"status\": \"True\",\n        \"type\": \"Initialized\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-12-31T19:27:25Z\",\n        \"status\": \"True\",\n        \"type\": \"Ready\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-12-31T19:27:25Z\",\n        \"status\": \"True\",\n        \"type\": \"ContainersReady\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-12-31T19:27:22Z\",\n        \"status\": \"True\",\n        \"type\": \"PodScheduled\"\n      }\n    ],\n    \"containerStatuses\": [\n      {\n        \"containerID\": \"docker://90e0abf7a779dd76d36038883312baed57a8351428a1d6340df3cff698f51809\",\n        \"image\": \"k8s.gcr.io/nginx-slim:0.8\",\n        \"imageID\": \"docker-pullable://k8s.gcr.io/nginx-slim@sha256:8b4501fe0fe221df663c22e16539f399e89594552f400408303c42f3dd8d0e52\",\n        \"lastState\": {},\n        \"name\": \"nginx\",\n        \"ready\": true,\n        \"restartCount\": 0,\n        \"state\": {\n          \"running\": {\n            \"startedAt\": \"2019-12-31T19:27:24Z\"\n          }\n        }\n      }\n    ],\n    \"hostIP\": \"10.128.0.15\",\n    \"phase\": \"Running\",\n    \"podIP\": \"10.44.0.229\",\n    \"qosClass\": \"Guaranteed\",\n    \"startTime\": \"2019-12-31T19:27:23Z\"\n  }\n}"
  },
  {
    "path": "internal/dao/testdata/secret.json",
    "content": "{\n    \"apiVersion\": \"v1\",\n    \"data\": {\n        \"token-secret\": \"MDEyMzQ1Njc4OWFiY2RlZg==\"\n    },\n    \"kind\": \"Secret\",\n    \"metadata\": {\n        \"creationTimestamp\": \"2024-01-15T18:19:00Z\",\n        \"name\": \"bootstrap-token-abcdef\",\n        \"namespace\": \"kube-system\",\n        \"resourceVersion\": \"243\",\n        \"uid\": \"6f5695d4-c0f4-4b65-890a-b1115ffd1f3b\"\n    },\n    \"type\": \"bootstrap.kubernetes.io/token\"\n}\n"
  },
  {
    "path": "internal/dao/types.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/watch\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/client-go/informers\"\n\trestclient \"k8s.io/client-go/rest\"\n)\n\n// Factory represents a resource factory.\ntype Factory interface {\n\t// Client retrieves an api client.\n\tClient() client.Connection\n\n\t// Get fetch a given resource.\n\tGet(gvr *client.GVR, path string, wait bool, sel labels.Selector) (runtime.Object, error)\n\n\t// List fetch a collection of resources.\n\tList(gvr *client.GVR, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error)\n\n\t// ForResource fetch an informer for a given resource.\n\tForResource(ns string, gvr *client.GVR) (informers.GenericInformer, error)\n\n\t// CanForResource fetch an informer for a given resource if authorized\n\tCanForResource(ns string, gvr *client.GVR, verbs []string) (informers.GenericInformer, error)\n\n\t// WaitForCacheSync synchronize the cache.\n\tWaitForCacheSync()\n\n\t// DeleteForwarder deletes a pod forwarder.\n\tDeleteForwarder(path string)\n\n\t// Forwarders returns all portforwards.\n\tForwarders() watch.Forwarders\n}\n\n// ImageLister tracks resources with container images.\ntype ImageLister interface {\n\t// ListImages lists container images.\n\tListImages(ctx context.Context, path string) ([]string, error)\n}\n\n// Getter represents a resource getter.\ntype Getter interface {\n\t// Get return a given resource.\n\tGet(ctx context.Context, path string) (runtime.Object, error)\n}\n\n// Lister represents a resource lister.\ntype Lister interface {\n\t// List returns a resource collection.\n\tList(ctx context.Context, ns string) ([]runtime.Object, error)\n}\n\n// Accessor represents an accessible k8s resource.\ntype Accessor interface {\n\tLister\n\tGetter\n\n\t// Init the resource with a factory object.\n\tInit(Factory, *client.GVR)\n\n\t// GVR returns a gvr a string.\n\tGVR() string\n\n\t// SetIncludeObject toggles object inclusion.\n\tSetIncludeObject(bool)\n}\n\n// DrainOptions tracks drain attributes.\ntype DrainOptions struct {\n\tGracePeriodSeconds  int\n\tTimeout             time.Duration\n\tIgnoreAllDaemonSets bool\n\tDeleteEmptyDirData  bool\n\tForce               bool\n\tDisableEviction     bool\n}\n\n// NodeMaintainer performs node maintenance operations.\ntype NodeMaintainer interface {\n\t// ToggleCordon toggles cordon/uncordon a node.\n\tToggleCordon(path string, cordon bool) error\n\n\t// Drain drains the given node.\n\tDrain(path string, opts DrainOptions, w io.Writer) error\n}\n\n// Loggable represents resources with logs.\ntype Loggable interface {\n\t// TailLogs streams resource logs.\n\tTailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error)\n}\n\n// Describer describes a resource.\ntype Describer interface {\n\t// Describe describes a resource.\n\tDescribe(path string) (string, error)\n\n\t// ToYAML dumps a resource to YAML.\n\tToYAML(path string, showManaged bool) (string, error)\n}\n\n// Scalable represents resources that can scale.\ntype Scalable interface {\n\t// Scale scales a resource up or down.\n\tScale(ctx context.Context, path string, replicas int32) error\n}\n\n// ReplicasGetter represents a resource with replicas.\ntype ReplicasGetter interface {\n\t// Replicas returns the number of replicas for the resource located at the given path.\n\tReplicas(ctx context.Context, path string) (int32, error)\n}\n\n// Controller represents a pod controller.\ntype Controller interface {\n\t// Pod returns a pod instance matching the selector.\n\tPod(path string) (string, error)\n}\n\n// Nuker represents a resource deleter.\ntype Nuker interface {\n\t// Delete removes a resource from the api server.\n\tDelete(context.Context, string, *metav1.DeletionPropagation, Grace) error\n}\n\n// Switchable represents a switchable resource.\ntype Switchable interface {\n\t// Switch changes the active context.\n\tSwitch(ctx string) error\n}\n\n// Restartable represents a restartable resource.\ntype Restartable interface {\n\t// Restart performs a rollout restart.\n\tRestart(context.Context, string, *metav1.PatchOptions) error\n}\n\n// Runnable represents a runnable resource.\ntype Runnable interface {\n\t// Run triggers a run.\n\tRun(path string) error\n}\n\n// Logger represents a resource that exposes logs.\ntype Logger interface {\n\t// Logs tails a resource logs.\n\tLogs(path string, opts *v1.PodLogOptions) (*restclient.Request, error)\n}\n\n// ContainsPodSpec represents a resource with a pod template.\ntype ContainsPodSpec interface {\n\t// GetPodSpec returns a podspec for the resource.\n\tGetPodSpec(path string) (*v1.PodSpec, error)\n\n\t// SetImages sets container image.\n\tSetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error\n}\n\n// Sanitizer represents a resource sanitizer.\ntype Sanitizer interface {\n\t// Sanitize nukes all resources in unhappy state.\n\tSanitize(context.Context, string) (int, error)\n}\n\n// Valuer represents a resource with values.\ntype Valuer interface {\n\t// GetValues returns values for a resource.\n\tGetValues(path string, allValues bool) ([]byte, error)\n}\n"
  },
  {
    "path": "internal/dao/utils_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao_test\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/watch\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/client-go/informers\"\n)\n\ntype testFactory struct {\n\tinventory map[string]map[*client.GVR][]runtime.Object\n}\n\nfunc makeFactory() dao.Factory {\n\treturn &testFactory{\n\t\tinventory: map[string]map[*client.GVR][]runtime.Object{\n\t\t\t\"kube-system\": {\n\t\t\t\tclient.SecGVR: {\n\t\t\t\t\tload(\"secret\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nvar _ dao.Factory = &testFactory{}\n\nfunc (*testFactory) Client() client.Connection {\n\treturn nil\n}\nfunc (f *testFactory) Get(gvr *client.GVR, fqn string, _ bool, _ labels.Selector) (runtime.Object, error) {\n\tns, po := path.Split(fqn)\n\tns = strings.Trim(ns, \"/\")\n\n\tfor _, o := range f.inventory[ns][gvr] {\n\t\tif o.(*unstructured.Unstructured).GetName() == po {\n\t\t\treturn o, nil\n\t\t}\n\t}\n\n\treturn nil, nil\n}\nfunc (f *testFactory) List(gvr *client.GVR, ns string, _ bool, _ labels.Selector) ([]runtime.Object, error) {\n\treturn f.inventory[ns][gvr], nil\n}\n\nfunc (*testFactory) ForResource(string, *client.GVR) (informers.GenericInformer, error) {\n\treturn nil, nil\n}\nfunc (*testFactory) CanForResource(string, *client.GVR, []string) (informers.GenericInformer, error) {\n\treturn nil, nil\n}\nfunc (*testFactory) WaitForCacheSync() {}\nfunc (*testFactory) Forwarders() watch.Forwarders {\n\treturn nil\n}\nfunc (*testFactory) DeleteForwarder(string) {}\n\nfunc load(n string) *unstructured.Unstructured {\n\traw, _ := os.ReadFile(fmt.Sprintf(\"testdata/%s.json\", n))\n\n\tvar o unstructured.Unstructured\n\t_ = json.Unmarshal(raw, &o)\n\n\treturn &o\n}\n"
  },
  {
    "path": "internal/dao/workload.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dao\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"k8s.io/apimachinery/pkg/api/meta\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nconst (\n\tStatusOK       = \"OK\"\n\tDegradedStatus = \"DEGRADED\"\n)\n\nvar resList = []*client.GVR{\n\tclient.PodGVR,\n\tclient.SvcGVR,\n\tclient.DsGVR,\n\tclient.StsGVR,\n\tclient.DpGVR,\n\tclient.RsGVR,\n}\n\n// Workload tracks a select set of resources in a given namespace.\ntype Workload struct {\n\tTable\n}\n\nfunc (w *Workload) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error {\n\tgvr, _ := ctx.Value(internal.KeyGVR).(*client.GVR)\n\tns, n := client.Namespaced(path)\n\tauth, err := w.Client().CanI(ns, gvr, n, []string{client.DeleteVerb})\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !auth {\n\t\treturn fmt.Errorf(\"user is not authorized to delete %s\", path)\n\t}\n\n\tvar gracePeriod *int64\n\tif grace != DefaultGrace {\n\t\tgracePeriod = (*int64)(&grace)\n\t}\n\topts := metav1.DeleteOptions{\n\t\tPropagationPolicy:  propagation,\n\t\tGracePeriodSeconds: gracePeriod,\n\t}\n\n\tctx, cancel := context.WithTimeout(ctx, w.Client().Config().CallTimeout())\n\tdefer cancel()\n\n\td, err := w.Client().DynDial()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdial := d.Resource(gvr.GVR())\n\tif client.IsClusterScoped(ns) {\n\t\treturn dial.Delete(ctx, n, opts)\n\t}\n\n\treturn dial.Namespace(ns).Delete(ctx, n, opts)\n}\n\nfunc (a *Workload) fetch(ctx context.Context, gvr *client.GVR, ns string) (*metav1.Table, error) {\n\ta.gvr = gvr\n\too, err := a.Table.List(ctx, ns)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(oo) == 0 {\n\t\treturn nil, fmt.Errorf(\"no table found for gvr: %s\", gvr)\n\t}\n\ttt, ok := oo[0].(*metav1.Table)\n\tif !ok {\n\t\treturn nil, errors.New(\"not a metav1.Table\")\n\t}\n\n\treturn tt, nil\n}\n\n// List fetch workloads.\nfunc (a *Workload) List(ctx context.Context, ns string) ([]runtime.Object, error) {\n\too := make([]runtime.Object, 0, 100)\n\tfor _, gvr := range resList {\n\t\ttable, err := a.fetch(ctx, gvr, ns)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvar (\n\t\t\tns string\n\t\t\tts metav1.Time\n\t\t)\n\t\tfor _, r := range table.Rows {\n\t\t\tif obj := r.Object.Object; obj != nil {\n\t\t\t\tif m, err := meta.Accessor(obj); err == nil {\n\t\t\t\t\tns, ts = m.GetNamespace(), m.GetCreationTimestamp()\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tvar m metav1.PartialObjectMetadata\n\t\t\t\tif err := json.Unmarshal(r.Object.Raw, &m); err == nil {\n\t\t\t\t\tns, ts = m.GetNamespace(), m.CreationTimestamp\n\t\t\t\t}\n\t\t\t}\n\t\t\tstat := status(gvr, &r, table.ColumnDefinitions)\n\t\t\too = append(oo, &render.WorkloadRes{Row: metav1.TableRow{Cells: []any{\n\t\t\t\tgvr.String(),\n\t\t\t\tns,\n\t\t\t\tr.Cells[indexOf(\"Name\", table.ColumnDefinitions)],\n\t\t\t\tstat,\n\t\t\t\treadiness(gvr, &r, table.ColumnDefinitions),\n\t\t\t\tvalidity(stat),\n\t\t\t\tts,\n\t\t\t}}})\n\t\t}\n\t}\n\n\treturn oo, nil\n}\n\n// Helpers...\n\nfunc readiness(gvr *client.GVR, r *metav1.TableRow, h []metav1.TableColumnDefinition) string {\n\tswitch gvr {\n\tcase client.PodGVR, client.DpGVR, client.StsGVR:\n\t\treturn r.Cells[indexOf(\"Ready\", h)].(string)\n\tcase client.RsGVR, client.DsGVR:\n\t\tc := r.Cells[indexOf(\"Ready\", h)].(int64)\n\t\td := r.Cells[indexOf(\"Desired\", h)].(int64)\n\t\treturn fmt.Sprintf(\"%d/%d\", c, d)\n\tcase client.SvcGVR:\n\t\treturn \"\"\n\t}\n\n\treturn render.NAValue\n}\n\nfunc status(gvr *client.GVR, r *metav1.TableRow, h []metav1.TableColumnDefinition) string {\n\tswitch gvr {\n\tcase client.PodGVR:\n\t\tif status := r.Cells[indexOf(\"Status\", h)]; status == render.PhaseCompleted {\n\t\t\treturn StatusOK\n\t\t} else if !isReady(r.Cells[indexOf(\"Ready\", h)].(string)) || status != render.PhaseRunning {\n\t\t\treturn DegradedStatus\n\t\t}\n\tcase client.DpGVR, client.StsGVR:\n\t\tif !isReady(r.Cells[indexOf(\"Ready\", h)].(string)) {\n\t\t\treturn DegradedStatus\n\t\t}\n\tcase client.RsGVR, client.DsGVR:\n\t\trd, ok1 := r.Cells[indexOf(\"Ready\", h)].(int64)\n\t\tde, ok2 := r.Cells[indexOf(\"Desired\", h)].(int64)\n\t\tif ok1 && ok2 {\n\t\t\tif !isReady(fmt.Sprintf(\"%d/%d\", rd, de)) {\n\t\t\t\treturn DegradedStatus\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\trds, oks1 := r.Cells[indexOf(\"Ready\", h)].(string)\n\t\tdes, oks2 := r.Cells[indexOf(\"Desired\", h)].(string)\n\t\tif oks1 && oks2 {\n\t\t\tif !isReady(fmt.Sprintf(\"%s/%s\", rds, des)) {\n\t\t\t\treturn DegradedStatus\n\t\t\t}\n\t\t}\n\tcase client.SvcGVR:\n\tdefault:\n\t\treturn render.MissingValue\n\t}\n\n\treturn StatusOK\n}\n\nfunc validity(status string) string {\n\tif status != \"DEGRADED\" {\n\t\treturn \"\"\n\t}\n\n\treturn status\n}\n\nfunc isReady(s string) bool {\n\ttt := strings.Split(s, \"/\")\n\tif len(tt) != 2 {\n\t\treturn false\n\t}\n\tr, err := strconv.Atoi(tt[0])\n\tif err != nil {\n\t\tslog.Error(\"Invalid ready count\",\n\t\t\tslogs.Error, err,\n\t\t\tslogs.Count, tt[0],\n\t\t)\n\t\treturn false\n\t}\n\tc, err := strconv.Atoi(tt[1])\n\tif err != nil {\n\t\tslog.Error(\"invalid expected count: %q\",\n\t\t\tslogs.Error, err,\n\t\t\tslogs.Count, tt[1],\n\t\t)\n\t\treturn false\n\t}\n\n\tif c == 0 {\n\t\treturn true\n\t}\n\treturn r == c\n}\n\nfunc indexOf(n string, defs []metav1.TableColumnDefinition) int {\n\tfor i, d := range defs {\n\t\tif d.Name == n {\n\t\t\treturn i\n\t\t}\n\t}\n\n\treturn -1\n}\n"
  },
  {
    "path": "internal/health/check.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage health\n\nimport (\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n)\n\n// Check tracks resource health.\ntype Check struct {\n\tCounts\n\n\tGVR *client.GVR\n}\n\n// Checks represents a collection of health checks.\ntype Checks []*Check\n\n// NewCheck returns a new health check.\nfunc NewCheck(gvr *client.GVR) *Check {\n\treturn &Check{\n\t\tGVR:    gvr,\n\t\tCounts: make(Counts),\n\t}\n}\n\n// Set sets a health metric.\nfunc (c *Check) Set(l Level, v int64) {\n\tc.Counts[l] = v\n}\n\n// Inc increments a health metric.\nfunc (c *Check) Inc(l Level) {\n\tc.Counts[l]++\n}\n\n// Total stores a metric total.\nfunc (c *Check) Total(n int64) {\n\tc.Counts[Corpus] = n\n}\n\n// Tally retrieves a given health metric.\nfunc (c *Check) Tally(l Level) int64 {\n\treturn c.Counts[l]\n}\n\n// GetObjectKind returns a schema object.\nfunc (Check) GetObjectKind() schema.ObjectKind {\n\treturn nil\n}\n\n// DeepCopyObject returns a container copy.\nfunc (c Check) DeepCopyObject() runtime.Object {\n\treturn c\n}\n"
  },
  {
    "path": "internal/health/check_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage health_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/health\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCheck(t *testing.T) {\n\tvar cc health.Checks\n\n\tc := health.NewCheck(client.NewGVR(\"test\"))\n\tn := 0\n\tfor range 10 {\n\t\tc.Inc(health.S1)\n\t\tcc = append(cc, c)\n\t\tn++\n\t}\n\tc.Total(int64(n))\n\n\tassert.Len(t, cc, 10)\n\tassert.Equal(t, int64(10), c.Tally(health.Corpus))\n\tassert.Equal(t, int64(10), c.Tally(health.S1))\n\tassert.Equal(t, int64(0), c.Tally(health.S2))\n}\n"
  },
  {
    "path": "internal/health/types.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage health\n\n// Level tracks health count categories.\ntype Level int\n\nconst (\n\t// Unknown represents no health level.\n\tUnknown Level = 1 << iota\n\n\t// Corpus tracks total health.\n\tCorpus\n\n\t// S1 tracks series 1.\n\tS1\n\n\t// S2 tracks series 2.\n\tS2\n\n\t// S3 tracks series 3.\n\tS3\n)\n\n// Message represents a health message.\ntype Message struct {\n\tLevel   Level\n\tMessage string\n\tGVR     string\n\tFQN     string\n}\n\n// Messages tracks a collection of messages.\ntype Messages []Message\n\n// Counts tracks health counts by category.\ntype Counts map[Level]int64\n\n// Vital tracks a resource vitals.\ntype Vital struct {\n\tResource         string\n\tTotal, OK, Toast int\n}\n\n// Vitals tracks a collection of resource health.\ntype Vitals []Vital\n"
  },
  {
    "path": "internal/helpers.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage internal\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n)\n\nvar (\n\tfuzzyRx = regexp.MustCompile(`\\A-f\\s?([\\w-]+)\\b`)\n\tlabelRx = regexp.MustCompile(`\\A\\-l`)\n)\n\n// Helpers...\n\n// IsInverseSelector checks if inverse char has been provided.\nfunc IsInverseSelector(s string) bool {\n\tif s == \"\" {\n\t\treturn false\n\t}\n\treturn s[0] == '!'\n}\n\n// IsLabelSelector checks if query is a label query.\nfunc IsLabelSelector(s string) bool {\n\tif labelRx.MatchString(s) {\n\t\treturn true\n\t}\n\n\treturn !strings.Contains(s, \" \") && cmd.ToLabels(s) != nil\n}\n\n// IsFuzzySelector checks if query is fuzzy.\nfunc IsFuzzySelector(s string) (string, bool) {\n\tmm := fuzzyRx.FindStringSubmatch(s)\n\tif len(mm) != 2 {\n\t\treturn \"\", false\n\t}\n\n\treturn mm[1], true\n}\n"
  },
  {
    "path": "internal/helpers_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage internal_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestIsLabelSelector(t *testing.T) {\n\tuu := map[string]struct {\n\t\ts  string\n\t\tok bool\n\t}{\n\t\t\"empty\":       {s: \"\"},\n\t\t\"cool\":        {s: \"-l app=fred,env=blee\", ok: true},\n\t\t\"no-flag\":     {s: \"app=fred,env=blee\", ok: true},\n\t\t\"no-space\":    {s: \"-lapp=fred,env=blee\", ok: true},\n\t\t\"wrong-flag\":  {s: \"-f app=fred,env=blee\"},\n\t\t\"missing-key\": {s: \"=fred\"},\n\t\t\"missing-val\": {s: \"fred=\"},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.ok, internal.IsLabelSelector(u.s))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/keys.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage internal\n\n// ContextKey represents context key.\ntype ContextKey string\n\n// A collection of context keys.\nconst (\n\tKeyFactory       ContextKey = \"factory\"\n\tKeyLabels        ContextKey = \"labels\"\n\tKeyFields        ContextKey = \"fields\"\n\tKeyTable         ContextKey = \"table\"\n\tKeyDir           ContextKey = \"dir\"\n\tKeyPath          ContextKey = \"path\"\n\tKeySubject       ContextKey = \"subject\"\n\tKeyGVR           ContextKey = \"gvr\"\n\tKeyFQN           ContextKey = \"fqn\"\n\tKeyForwards      ContextKey = \"forwards\"\n\tKeyContainers    ContextKey = \"containers\"\n\tKeyBenchCfg      ContextKey = \"benchcfg\"\n\tKeyAliases       ContextKey = \"aliases\"\n\tKeyUID           ContextKey = \"uid\"\n\tKeySubjectKind   ContextKey = \"subjectKind\"\n\tKeySubjectName   ContextKey = \"subjectName\"\n\tKeyNamespace     ContextKey = \"namespace\"\n\tKeyCluster       ContextKey = \"cluster\"\n\tKeyApp           ContextKey = \"app\"\n\tKeyStyles        ContextKey = \"styles\"\n\tKeyMetrics       ContextKey = \"metrics\"\n\tKeyHasMetrics    ContextKey = \"has-metrics\"\n\tKeyToast         ContextKey = \"toast\"\n\tKeyWithMetrics   ContextKey = \"withMetrics\"\n\tKeyViewConfig    ContextKey = \"viewConfig\"\n\tKeyWait          ContextKey = \"wait\"\n\tKeyPodCounting   ContextKey = \"podCounting\"\n\tKeyEnableImgScan ContextKey = \"vulScan\"\n)\n"
  },
  {
    "path": "internal/model/cluster.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/util/cache\"\n\tmv1beta1 \"k8s.io/metrics/pkg/apis/metrics/v1beta1\"\n)\n\nconst (\n\tclusterCacheSize   = 100\n\tclusterCacheExpiry = 1 * time.Minute\n\tclusterNodesKey    = \"nodes\"\n)\n\ntype (\n\t// MetricsServer gather metrics information from pods and nodes.\n\tMetricsServer interface {\n\t\tMetricsService\n\n\t\tClusterLoad(*v1.NodeList, *mv1beta1.NodeMetricsList, *client.ClusterMetrics) error\n\t\tNodesMetrics(*v1.NodeList, *mv1beta1.NodeMetricsList, client.NodesMetrics)\n\t\tPodsMetrics(*mv1beta1.PodMetricsList, client.PodsMetrics)\n\t}\n\n\t// MetricsService calls the metrics server for metrics info.\n\tMetricsService interface {\n\t\tHasMetrics() bool\n\t\tFetchNodesMetrics(ctx context.Context) (*mv1beta1.NodeMetricsList, error)\n\t\tFetchPodsMetrics(ctx context.Context, ns string) (*mv1beta1.PodMetricsList, error)\n\t}\n\n\t// Cluster represents a kubernetes resource.\n\tCluster struct {\n\t\tfactory dao.Factory\n\t\tmx      MetricsServer\n\t\tcache   *cache.LRUExpireCache\n\t}\n)\n\n// NewCluster returns a new cluster info resource.\nfunc NewCluster(f dao.Factory) *Cluster {\n\treturn &Cluster{\n\t\tfactory: f,\n\t\tmx:      client.DialMetrics(f.Client()),\n\t\tcache:   cache.NewLRUExpireCache(clusterCacheSize),\n\t}\n}\n\n// Version returns the current K8s cluster version.\nfunc (c *Cluster) Version() string {\n\tinfo, err := c.factory.Client().ServerVersion()\n\tif err != nil || info == nil {\n\t\treturn client.NA\n\t}\n\n\treturn info.GitVersion\n}\n\n// ContextName returns the context name.\nfunc (c *Cluster) ContextName() string {\n\tn, err := c.factory.Client().Config().CurrentContextName()\n\tif err != nil {\n\t\treturn client.NA\n\t}\n\treturn n\n}\n\n// ClusterName returns the context name.\nfunc (c *Cluster) ClusterName() string {\n\tn, err := c.factory.Client().Config().CurrentClusterName()\n\tif err != nil {\n\t\treturn client.NA\n\t}\n\treturn n\n}\n\n// UserName returns the user name.\nfunc (c *Cluster) UserName() string {\n\tn, err := c.factory.Client().Config().CurrentUserName()\n\tif err != nil {\n\t\treturn client.NA\n\t}\n\treturn n\n}\n\n// Metrics gathers node level metrics and compute utilization percentages.\nfunc (c *Cluster) Metrics(ctx context.Context, mx *client.ClusterMetrics) error {\n\tvar (\n\t\tnn  *v1.NodeList\n\t\terr error\n\t)\n\tif v, ok := c.cache.Get(clusterNodesKey); ok {\n\t\tif nl, ok := v.(*v1.NodeList); ok {\n\t\t\tnn = nl\n\t\t}\n\t} else {\n\t\tif nn, err = dao.FetchNodes(ctx, c.factory, \"\"); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tif nn == nil {\n\t\treturn errors.New(\"unable to fetch nodes list\")\n\t}\n\tif len(nn.Items) > 0 {\n\t\tc.cache.Add(clusterNodesKey, nn, clusterCacheExpiry)\n\t}\n\tvar nmx *mv1beta1.NodeMetricsList\n\tif nmx, err = c.mx.FetchNodesMetrics(ctx); err != nil {\n\t\treturn err\n\t}\n\n\treturn c.mx.ClusterLoad(nn, nmx, mx)\n}\n"
  },
  {
    "path": "internal/model/cluster_info.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"k8s.io/apimachinery/pkg/util/cache\"\n)\n\nconst (\n\tk9sGitURL       = \"https://api.github.com/repos/derailed/k9s/releases/latest\"\n\tcacheSize       = 10\n\tcacheExpiry     = 1 * time.Hour\n\tk9sLatestRevKey = \"k9sRev\"\n)\n\n// ClusterInfoListener registers a listener for model changes.\ntype ClusterInfoListener interface {\n\t// ClusterInfoChanged notifies the cluster meta was changed.\n\tClusterInfoChanged(prev, curr *ClusterMeta)\n\n\t// ClusterInfoUpdated notifies the cluster meta was updated.\n\tClusterInfoUpdated(*ClusterMeta)\n}\n\n// ClusterMeta represents cluster meta data.\ntype ClusterMeta struct {\n\tContext, Cluster    string\n\tUser                string\n\tK9sVer, K9sLatest   string\n\tK8sVer              string\n\tCpu, Mem, Ephemeral int\n}\n\n// NewClusterMeta returns a new instance.\nfunc NewClusterMeta() *ClusterMeta {\n\treturn &ClusterMeta{\n\t\tContext:   client.NA,\n\t\tCluster:   client.NA,\n\t\tUser:      client.NA,\n\t\tK9sVer:    client.NA,\n\t\tK8sVer:    client.NA,\n\t\tCpu:       0,\n\t\tMem:       0,\n\t\tEphemeral: 0,\n\t}\n}\n\n// Deltas diffs cluster meta return true if different, false otherwise.\nfunc (c *ClusterMeta) Deltas(n *ClusterMeta) bool {\n\tif c.Cpu != n.Cpu || c.Mem != n.Mem || c.Ephemeral != n.Ephemeral {\n\t\treturn true\n\t}\n\n\treturn c.Context != n.Context ||\n\t\tc.Cluster != n.Cluster ||\n\t\tc.User != n.User ||\n\t\tc.K8sVer != n.K8sVer ||\n\t\tc.K9sVer != n.K9sVer ||\n\t\tc.K9sLatest != n.K9sLatest\n}\n\n// ClusterInfo models cluster metadata.\ntype ClusterInfo struct {\n\tcluster   *Cluster\n\tfactory   dao.Factory\n\tdata      *ClusterMeta\n\tversion   string\n\tcfg       *config.K9s\n\tlisteners []ClusterInfoListener\n\tcache     *cache.LRUExpireCache\n\tmx        sync.RWMutex\n}\n\n// NewClusterInfo returns a new instance.\nfunc NewClusterInfo(f dao.Factory, v string, cfg *config.K9s) *ClusterInfo {\n\tc := ClusterInfo{\n\t\tfactory: f,\n\t\tcluster: NewCluster(f),\n\t\tdata:    NewClusterMeta(),\n\t\tversion: v,\n\t\tcfg:     cfg,\n\t\tcache:   cache.NewLRUExpireCache(cacheSize),\n\t}\n\n\treturn &c\n}\n\nfunc (c *ClusterInfo) fetchK9sLatestRev() string {\n\trev, ok := c.cache.Get(k9sLatestRevKey)\n\tif ok {\n\t\treturn rev.(string)\n\t}\n\n\tlatestRev, err := fetchLatestRev()\n\tif err != nil {\n\t\tslog.Warn(\"k9s latest rev fetch failed\", slogs.Error, err)\n\t} else {\n\t\tc.cache.Add(k9sLatestRevKey, latestRev, cacheExpiry)\n\t}\n\n\treturn latestRev\n}\n\n// Reset resets context and reload.\nfunc (c *ClusterInfo) Reset(f dao.Factory) {\n\tif f == nil {\n\t\treturn\n\t}\n\n\tc.mx.Lock()\n\tc.cluster, c.data = NewCluster(f), NewClusterMeta()\n\tc.mx.Unlock()\n\n\tc.Refresh()\n}\n\n// Refresh fetches the latest cluster meta.\nfunc (c *ClusterInfo) Refresh() {\n\tdata := NewClusterMeta()\n\tif c.factory.Client().ConnectionOK() {\n\t\tdata.Context = c.cluster.ContextName()\n\t\tdata.Cluster = c.cluster.ClusterName()\n\t\tdata.User = c.cluster.UserName()\n\t\tdata.K8sVer = c.cluster.Version()\n\t\tctx, cancel := context.WithTimeout(context.Background(), c.cluster.factory.Client().Config().CallTimeout())\n\t\tdefer cancel()\n\t\tvar mx client.ClusterMetrics\n\t\tif err := c.cluster.Metrics(ctx, &mx); err == nil {\n\t\t\tdata.Cpu, data.Mem, data.Ephemeral = mx.PercCPU, mx.PercMEM, mx.PercEphemeral\n\t\t}\n\t}\n\tdata.K9sVer = c.version\n\tv1 := NewSemVer(data.K9sVer)\n\n\tvar latestRev string\n\tif !c.cfg.SkipLatestRevCheck {\n\t\tlatestRev = c.fetchK9sLatestRev()\n\t}\n\tv2 := NewSemVer(latestRev)\n\n\tdata.K9sVer, data.K9sLatest = v1.String(), v2.String()\n\tif v1.IsCurrent(v2) {\n\t\tdata.K9sLatest = \"\"\n\t}\n\n\tif c.data.Deltas(data) {\n\t\tc.fireMetaChanged(c.data, data)\n\t} else {\n\t\tc.fireNoMetaChanged(data)\n\t}\n\tc.mx.Lock()\n\tc.data = data\n\tc.mx.Unlock()\n}\n\n// AddListener adds a new model listener.\nfunc (c *ClusterInfo) AddListener(l ClusterInfoListener) {\n\tc.listeners = append(c.listeners, l)\n}\n\n// RemoveListener delete a listener from the list.\nfunc (c *ClusterInfo) RemoveListener(l ClusterInfoListener) {\n\tvictim := -1\n\tfor i, lis := range c.listeners {\n\t\tif lis == l {\n\t\t\tvictim = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif victim >= 0 {\n\t\tc.listeners = append(c.listeners[:victim], c.listeners[victim+1:]...)\n\t}\n}\n\nfunc (c *ClusterInfo) fireMetaChanged(prev, cur *ClusterMeta) {\n\tfor _, l := range c.listeners {\n\t\tl.ClusterInfoChanged(prev, cur)\n\t}\n}\n\nfunc (c *ClusterInfo) fireNoMetaChanged(data *ClusterMeta) {\n\tfor _, l := range c.listeners {\n\t\tl.ClusterInfoUpdated(data)\n\t}\n}\n\n// Helpers...\n\nfunc fetchLatestRev() (string, error) {\n\tslog.Debug(\"Fetching latest k9s rev...\")\n\tctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)\n\tdefer cancel()\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodGet, k9sGitURL, http.NoBody)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tresp, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer func() {\n\t\tif resp.Body != nil {\n\t\t\t_ = resp.Body.Close()\n\t\t}\n\t}()\n\n\tb, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tm := make(map[string]any, 20)\n\tif err := json.Unmarshal(b, &m); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif v, ok := m[\"name\"]; ok {\n\t\tslog.Debug(\"K9s latest rev\", slogs.Revision, v.(string))\n\t\treturn v.(string), nil\n\t}\n\n\treturn \"\", errors.New(\"no version found\")\n}\n"
  },
  {
    "path": "internal/model/cluster_info_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc init() {\n\tslog.SetDefault(slog.New(slog.DiscardHandler))\n}\n\nfunc TestClusterMetaDelta(t *testing.T) {\n\tuu := map[string]struct {\n\t\to, n *model.ClusterMeta\n\t\te    bool\n\t}{\n\t\t\"empty\": {\n\t\t\to: model.NewClusterMeta(),\n\t\t\tn: model.NewClusterMeta(),\n\t\t},\n\t\t\"same\": {\n\t\t\to: makeClusterMeta(\"fred\"),\n\t\t\tn: makeClusterMeta(\"fred\"),\n\t\t},\n\t\t\"diff\": {\n\t\t\to: makeClusterMeta(\"fred\"),\n\t\t\tn: makeClusterMeta(\"freddie\"),\n\t\t\te: true,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.o.Deltas(u.n))\n\t\t})\n\t}\n}\n\n// Helpers...\n\nfunc makeClusterMeta(cluster string) *model.ClusterMeta {\n\tm := model.NewClusterMeta()\n\tm.Cluster = cluster\n\tm.Cpu, m.Mem = 10, 20\n\n\treturn m\n}\n"
  },
  {
    "path": "internal/model/cmd_buff.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"time\"\n)\n\nconst (\n\tmaxBuff = 10\n\n\tkeyEntryDelay = 100 * time.Millisecond\n\n\t// CommandBuffer represents a command buffer.\n\tCommandBuffer BufferKind = 1 << iota\n\t// FilterBuffer represents a filter buffer.\n\tFilterBuffer\n)\n\ntype (\n\t// BufferKind indicates a buffer type.\n\tBufferKind int8\n\n\t// BuffWatcher represents a command buffer listener.\n\tBuffWatcher interface {\n\t\t// BufferCompleted indicates input was accepted.\n\t\tBufferCompleted(text, suggestion string)\n\n\t\t// BufferChanged indicates the buffer was changed.\n\t\tBufferChanged(text, suggestion string)\n\n\t\t// BufferActive indicates the buff activity changed.\n\t\tBufferActive(state bool, kind BufferKind)\n\t}\n)\n\n// CmdBuff represents user command input.\ntype CmdBuff struct {\n\tbuff       []rune\n\tsuggestion string\n\tlisteners  map[BuffWatcher]struct{}\n\thotKey     rune\n\tkind       BufferKind\n\tactive     bool\n\tcancel     context.CancelFunc\n\tmx         sync.RWMutex\n}\n\n// NewCmdBuff returns a new command buffer.\nfunc NewCmdBuff(key rune, kind BufferKind) *CmdBuff {\n\treturn &CmdBuff{\n\t\thotKey:    key,\n\t\tkind:      kind,\n\t\tbuff:      make([]rune, 0, maxBuff),\n\t\tlisteners: make(map[BuffWatcher]struct{}),\n\t}\n}\n\n// InCmdMode checks if a command exists and the buffer is active.\nfunc (c *CmdBuff) InCmdMode() bool {\n\tc.mx.RLock()\n\tdefer c.mx.RUnlock()\n\n\tif !c.active {\n\t\treturn false\n\t}\n\n\treturn len(c.buff) > 0\n}\n\n// IsActive checks if command buffer is active.\nfunc (c *CmdBuff) IsActive() bool {\n\tc.mx.RLock()\n\tdefer c.mx.RUnlock()\n\n\treturn c.active\n}\n\n// SetActive toggles cmd buffer active state.\nfunc (c *CmdBuff) SetActive(b bool) {\n\tc.mx.Lock()\n\tc.active = b\n\tc.mx.Unlock()\n\n\tc.fireActive(c.active)\n}\n\n// GetText returns the current text.\nfunc (c *CmdBuff) GetText() string {\n\tc.mx.RLock()\n\tdefer c.mx.RUnlock()\n\n\treturn string(c.buff)\n}\n\n// GetKind returns the buffer kind.\nfunc (c *CmdBuff) GetKind() BufferKind {\n\tc.mx.RLock()\n\tdefer c.mx.RUnlock()\n\n\treturn c.kind\n}\n\n// GetSuggestion returns the current suggestion.\nfunc (c *CmdBuff) GetSuggestion() string {\n\tc.mx.RLock()\n\tdefer c.mx.RUnlock()\n\n\treturn c.suggestion\n}\n\nfunc (c *CmdBuff) hasCancel() bool {\n\tc.mx.RLock()\n\tdefer c.mx.RUnlock()\n\n\treturn c.cancel != nil\n}\n\nfunc (c *CmdBuff) setCancel(f context.CancelFunc) {\n\tc.mx.Lock()\n\tc.cancel = f\n\tc.mx.Unlock()\n}\n\nfunc (c *CmdBuff) resetCancel() {\n\tc.mx.Lock()\n\tc.cancel = nil\n\tc.mx.Unlock()\n}\n\n// SetText initializes the buffer with a command.\nfunc (c *CmdBuff) SetText(text, suggestion string, wipe bool) {\n\tc.mx.Lock()\n\tif wipe {\n\t\tc.buff, c.suggestion = []rune(text), suggestion\n\t} else {\n\t\tc.buff, c.suggestion = append(c.buff, []rune(text)...), suggestion\n\t}\n\tc.mx.Unlock()\n\tc.fireBufferCompleted(c.GetText(), c.GetSuggestion())\n}\n\n// Add adds a new character to the buffer.\nfunc (c *CmdBuff) Add(r rune) {\n\tc.mx.Lock()\n\tc.buff = append(c.buff, r)\n\tc.mx.Unlock()\n\tc.fireBufferChanged(c.GetText(), c.GetSuggestion())\n\tif c.hasCancel() {\n\t\treturn\n\t}\n\tctx, cancel := context.WithTimeout(context.Background(), keyEntryDelay)\n\tc.setCancel(cancel)\n\n\tgo func() {\n\t\t<-ctx.Done()\n\t\tc.fireBufferCompleted(c.GetText(), c.GetSuggestion())\n\t\tc.resetCancel()\n\t}()\n}\n\n// Delete removes the last character from the buffer.\nfunc (c *CmdBuff) Delete() {\n\tif c.Empty() {\n\t\treturn\n\t}\n\tc.SetText(string(c.buff[:len(c.buff)-1]), \"\", true)\n\tc.fireBufferChanged(c.GetText(), c.GetSuggestion())\n\tif c.hasCancel() {\n\t\treturn\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), 800*time.Millisecond)\n\tc.setCancel(cancel)\n\n\tgo func() {\n\t\t<-ctx.Done()\n\t\tc.fireBufferCompleted(c.GetText(), c.GetSuggestion())\n\t\tc.resetCancel()\n\t}()\n}\n\n// ClearText clears out command buffer.\nfunc (c *CmdBuff) ClearText(fire bool) {\n\tc.mx.Lock()\n\tc.buff, c.suggestion = c.buff[:0], \"\"\n\tc.mx.Unlock()\n\n\tif fire {\n\t\tc.fireBufferCompleted(c.GetText(), c.GetSuggestion())\n\t}\n}\n\n// Reset clears out the command buffer and deactivates it.\nfunc (c *CmdBuff) Reset() {\n\tc.ClearText(true)\n\tc.SetActive(false)\n\tc.fireBufferCompleted(c.GetText(), c.GetSuggestion())\n}\n\n// Empty returns true if no cmd, false otherwise.\nfunc (c *CmdBuff) Empty() bool {\n\tc.mx.RLock()\n\tdefer c.mx.RUnlock()\n\n\treturn len(c.buff) == 0\n}\n\n// ----------------------------------------------------------------------------\n// Event Listeners...\n\n// AddListener registers a cmd buffer listener.\nfunc (c *CmdBuff) AddListener(w BuffWatcher) {\n\tc.mx.Lock()\n\tc.listeners[w] = struct{}{}\n\tc.mx.Unlock()\n}\n\n// RemoveListener removes a listener.\nfunc (c *CmdBuff) RemoveListener(l BuffWatcher) {\n\tc.mx.Lock()\n\tdelete(c.listeners, l)\n\tc.mx.Unlock()\n}\n\nfunc (c *CmdBuff) fireBufferCompleted(t, s string) {\n\tfor l := range c.listeners {\n\t\tl.BufferCompleted(t, s)\n\t}\n}\n\nfunc (c *CmdBuff) fireBufferChanged(t, s string) {\n\tfor l := range c.listeners {\n\t\tl.BufferChanged(t, s)\n\t}\n}\n\nfunc (c *CmdBuff) fireActive(b bool) {\n\tfor l := range c.listeners {\n\t\tl.BufferActive(b, c.GetKind())\n\t}\n}\n"
  },
  {
    "path": "internal/model/cmd_buff_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\ntype testListener struct {\n\ttext, suggestion string\n\tact              int\n\tinact            int\n}\n\nfunc (l *testListener) BufferChanged(t, s string) {\n\tl.text, l.suggestion = t, s\n}\n\nfunc (l *testListener) BufferCompleted(t, s string) {\n\tl.text, l.suggestion = t, s\n}\n\nfunc (l *testListener) BufferActive(s bool, _ model.BufferKind) {\n\tif s {\n\t\tl.act++\n\t\treturn\n\t}\n\tl.inact++\n}\n\nfunc TestCmdBuffActivate(t *testing.T) {\n\tb, l := model.NewCmdBuff('>', model.CommandBuffer), testListener{}\n\tb.AddListener(&l)\n\n\tb.SetActive(true)\n\tassert.Equal(t, 1, l.act)\n\tassert.Equal(t, 0, l.inact)\n\tassert.True(t, b.IsActive())\n}\n\nfunc TestCmdBuffDeactivate(t *testing.T) {\n\tb, l := model.NewCmdBuff('>', model.CommandBuffer), testListener{}\n\tb.AddListener(&l)\n\n\tb.SetActive(false)\n\tassert.Equal(t, 0, l.act)\n\tassert.Equal(t, 1, l.inact)\n\tassert.False(t, b.IsActive())\n}\n\nfunc TestCmdBuffChanged(t *testing.T) {\n\tb, l := model.NewCmdBuff('>', model.CommandBuffer), testListener{}\n\tb.AddListener(&l)\n\n\tb.Add('b')\n\tassert.Equal(t, 0, l.act)\n\tassert.Equal(t, 0, l.inact)\n\tassert.Equal(t, \"b\", l.text)\n\tassert.Equal(t, \"b\", b.GetText())\n\n\tb.Delete()\n\tassert.Equal(t, 0, l.act)\n\tassert.Equal(t, 0, l.inact)\n\tassert.Empty(t, l.text)\n\tassert.Empty(t, b.GetText())\n\n\tb.Add('c')\n\tb.ClearText(true)\n\tassert.Equal(t, 0, l.act)\n\tassert.Equal(t, 0, l.inact)\n\tassert.Empty(t, l.text)\n\tassert.Empty(t, b.GetText())\n\n\tb.Add('c')\n\tb.Reset()\n\tassert.Equal(t, 0, l.act)\n\tassert.Equal(t, 1, l.inact)\n\tassert.Empty(t, l.text)\n\tassert.Empty(t, b.GetText())\n\tassert.True(t, b.Empty())\n}\n\nfunc TestCmdBuffAdd(t *testing.T) {\n\tb := model.NewCmdBuff('>', model.CommandBuffer)\n\n\tuu := []struct {\n\t\trunes []rune\n\t\tcmd   string\n\t}{\n\t\t{[]rune{}, \"\"},\n\t\t{[]rune{'a'}, \"a\"},\n\t\t{[]rune{'a', 'b', 'c'}, \"abc\"},\n\t}\n\n\tfor _, u := range uu {\n\t\tfor _, r := range u.runes {\n\t\t\tb.Add(r)\n\t\t}\n\t\tassert.Equal(t, u.cmd, b.GetText())\n\t\tb.Reset()\n\t}\n}\n\nfunc TestCmdBuffDel(t *testing.T) {\n\tb := model.NewCmdBuff('>', model.CommandBuffer)\n\n\tuu := []struct {\n\t\trunes []rune\n\t\tcmd   string\n\t}{\n\t\t{[]rune{}, \"\"},\n\t\t{[]rune{'a'}, \"\"},\n\t\t{[]rune{'a', 'b', 'c'}, \"ab\"},\n\t}\n\n\tfor _, u := range uu {\n\t\tfor _, r := range u.runes {\n\t\t\tb.Add(r)\n\t\t}\n\t\tb.Delete()\n\t\tassert.Equal(t, u.cmd, b.GetText())\n\t\tb.Reset()\n\t}\n}\n\nfunc TestCmdBuffEmpty(t *testing.T) {\n\tb := model.NewCmdBuff('>', model.CommandBuffer)\n\n\tuu := []struct {\n\t\trunes []rune\n\t\tempty bool\n\t}{\n\t\t{[]rune{}, true},\n\t\t{[]rune{'a'}, false},\n\t\t{[]rune{'a', 'b', 'c'}, false},\n\t}\n\n\tfor _, u := range uu {\n\t\tfor _, r := range u.runes {\n\t\t\tb.Add(r)\n\t\t}\n\t\tassert.Equal(t, u.empty, b.Empty())\n\t\tb.Reset()\n\t}\n}\n"
  },
  {
    "path": "internal/model/describe.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"reflect\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\tbackoff \"github.com/cenkalti/backoff/v4\"\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/sahilm/fuzzy\"\n)\n\n// Describe tracks describable resources.\ntype Describe struct {\n\tgvr         *client.GVR\n\tinUpdate    int32\n\tpath        string\n\tquery       string\n\tlines       []string\n\trefreshRate time.Duration\n\tlisteners   []ResourceViewerListener\n\tdecode      bool\n}\n\n// NewDescribe returns a new describe resource model.\nfunc NewDescribe(gvr *client.GVR, path string) *Describe {\n\treturn &Describe{\n\t\tgvr:         gvr,\n\t\tpath:        path,\n\t\trefreshRate: defaultReaderRefreshRate,\n\t}\n}\n\n// GVR returns the resource gvr.\nfunc (d *Describe) GVR() *client.GVR {\n\treturn d.gvr\n}\n\n// GetPath returns the active resource path.\nfunc (d *Describe) GetPath() string {\n\treturn d.path\n}\n\n// SetOptions toggle model options.\nfunc (*Describe) SetOptions(context.Context, ViewerToggleOpts) {}\n\n// Filter filters the model.\nfunc (d *Describe) Filter(q string) {\n\td.query = q\n\td.filterChanged(d.lines)\n}\n\nfunc (d *Describe) filterChanged(lines []string) {\n\td.fireResourceChanged(lines, d.filter(d.query, lines))\n}\n\nfunc (d *Describe) filter(q string, lines []string) fuzzy.Matches {\n\tif q == \"\" {\n\t\treturn nil\n\t}\n\tif f, ok := internal.IsFuzzySelector(q); ok {\n\t\treturn d.fuzzyFilter(strings.TrimSpace(f), lines)\n\t}\n\treturn rxFilter(q, lines)\n}\n\nfunc (*Describe) fuzzyFilter(q string, lines []string) fuzzy.Matches {\n\treturn fuzzy.Find(q, lines)\n}\n\nfunc (d *Describe) fireResourceChanged(lines []string, matches fuzzy.Matches) {\n\tfor _, l := range d.listeners {\n\t\tl.ResourceChanged(lines, matches)\n\t}\n}\n\nfunc (d *Describe) fireResourceFailed(err error) {\n\tfor _, l := range d.listeners {\n\t\tl.ResourceFailed(err)\n\t}\n}\n\n// ClearFilter clear out the filter.\nfunc (*Describe) ClearFilter() {\n}\n\n// Peek returns current model state.\nfunc (d *Describe) Peek() []string {\n\treturn d.lines\n}\n\n// Refresh updates model data.\nfunc (d *Describe) Refresh(ctx context.Context) error {\n\treturn d.refresh(ctx)\n}\n\n// Watch watches for describe data changes.\nfunc (d *Describe) Watch(ctx context.Context) error {\n\tif err := d.refresh(ctx); err != nil {\n\t\treturn err\n\t}\n\tgo d.updater(ctx)\n\treturn nil\n}\n\nfunc (d *Describe) updater(ctx context.Context) {\n\tdefer slog.Debug(\"Describe canceled\", slogs.GVR, d.gvr)\n\n\tbackOff := NewExpBackOff(ctx, defaultReaderRefreshRate, maxReaderRetryInterval)\n\tdelay := defaultReaderRefreshRate\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-time.After(delay):\n\t\t\tif err := d.refresh(ctx); err != nil {\n\t\t\t\td.fireResourceFailed(err)\n\t\t\t\tif delay = backOff.NextBackOff(); delay == backoff.Stop {\n\t\t\t\t\tslog.Error(\"Describe gave up!\", slogs.Error, err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tbackOff.Reset()\n\t\t\t\tdelay = defaultReaderRefreshRate\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (d *Describe) refresh(ctx context.Context) error {\n\tif !atomic.CompareAndSwapInt32(&d.inUpdate, 0, 1) {\n\t\tslog.Debug(\"Dropping update...\")\n\t\treturn nil\n\t}\n\tdefer atomic.StoreInt32(&d.inUpdate, 0)\n\n\tif err := d.reconcile(ctx); err != nil {\n\t\tslog.Error(\"reconcile failed\",\n\t\t\tslogs.GVR, d.gvr,\n\t\t\tslogs.Error, err,\n\t\t)\n\t\td.fireResourceFailed(err)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Describe) reconcile(ctx context.Context) error {\n\ts, err := d.describe(ctx, d.gvr, d.path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tlines := strings.Split(s, \"\\n\")\n\tif reflect.DeepEqual(lines, d.lines) {\n\t\treturn nil\n\t}\n\td.lines = lines\n\td.fireResourceChanged(d.lines, d.filter(d.query, d.lines))\n\n\treturn nil\n}\n\n// Describe describes a given resource.\nfunc (d *Describe) describe(ctx context.Context, gvr *client.GVR, path string) (string, error) {\n\tmeta, err := getMeta(ctx, gvr)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdesc, ok := meta.DAO.(dao.Describer)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"no describer for %q\", meta.DAO.GVR())\n\t}\n\tif desc, ok := meta.DAO.(*dao.Secret); ok {\n\t\tdesc.SetDecodeData(d.decode)\n\t}\n\n\treturn desc.Describe(path)\n}\n\n// AddListener adds a new model listener.\nfunc (d *Describe) AddListener(l ResourceViewerListener) {\n\td.listeners = append(d.listeners, l)\n}\n\n// RemoveListener delete a listener from the list.\nfunc (d *Describe) RemoveListener(l ResourceViewerListener) {\n\tvictim := -1\n\tfor i, lis := range d.listeners {\n\t\tif lis == l {\n\t\t\tvictim = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif victim >= 0 {\n\t\td.listeners = append(d.listeners[:victim], d.listeners[victim+1:]...)\n\t}\n}\n\n// Toggle toggles the decode flag.\nfunc (d *Describe) Toggle() {\n\td.decode = !d.decode\n}\n"
  },
  {
    "path": "internal/model/fish_buff.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport \"sort\"\n\n// SuggestionListener listens for suggestions.\ntype SuggestionListener interface {\n\tBuffWatcher\n\n\t// SuggestionChanged notifies suggestion changes.\n\tSuggestionChanged(text, sugg string)\n}\n\n// SuggestionFunc produces suggestions.\ntype SuggestionFunc func(text string) sort.StringSlice\n\n// FishBuff represents a suggestion buffer.\ntype FishBuff struct {\n\t*CmdBuff\n\n\tsuggestionFn    SuggestionFunc\n\tsuggestions     []string\n\tsuggestionIndex int\n}\n\n// NewFishBuff returns a new command buffer.\nfunc NewFishBuff(key rune, kind BufferKind) *FishBuff {\n\treturn &FishBuff{\n\t\tCmdBuff:         NewCmdBuff(key, kind),\n\t\tsuggestionIndex: -1,\n\t}\n}\n\n// PrevSuggestion returns the prev suggestion.\nfunc (f *FishBuff) PrevSuggestion() (string, bool) {\n\tif len(f.suggestions) == 0 {\n\t\treturn \"\", false\n\t}\n\n\tif f.suggestionIndex < 0 {\n\t\tf.suggestionIndex = 0\n\t} else {\n\t\tf.suggestionIndex--\n\t}\n\tif f.suggestionIndex < 0 {\n\t\tf.suggestionIndex = len(f.suggestions) - 1\n\t}\n\n\treturn f.suggestions[f.suggestionIndex], true\n}\n\n// NextSuggestion returns the next suggestion.\nfunc (f *FishBuff) NextSuggestion() (string, bool) {\n\tif len(f.suggestions) == 0 {\n\t\treturn \"\", false\n\t}\n\n\tif f.suggestionIndex < 0 {\n\t\tf.suggestionIndex = 0\n\t} else {\n\t\tf.suggestionIndex++\n\t}\n\tif f.suggestionIndex >= len(f.suggestions) {\n\t\tf.suggestionIndex = 0\n\t}\n\n\treturn f.suggestions[f.suggestionIndex], true\n}\n\n// ClearSuggestions clear out all suggestions.\nfunc (f *FishBuff) ClearSuggestions() {\n\tif len(f.suggestions) > 0 {\n\t\tf.suggestions = f.suggestions[:0]\n\t}\n\tf.suggestionIndex = -1\n}\n\n// CurrentSuggestion returns the current suggestion.\nfunc (f *FishBuff) CurrentSuggestion() (string, bool) {\n\tif len(f.suggestions) == 0 || f.suggestionIndex < 0 || f.suggestionIndex >= len(f.suggestions) {\n\t\treturn \"\", false\n\t}\n\n\treturn f.suggestions[f.suggestionIndex], true\n}\n\n// AutoSuggests returns true if model implements auto suggestions.\nfunc (*FishBuff) AutoSuggests() bool {\n\treturn true\n}\n\n// Suggestions returns suggestions.\nfunc (f *FishBuff) Suggestions() []string {\n\tif f.suggestionFn != nil {\n\t\treturn f.suggestionFn(string(f.buff))\n\t}\n\treturn nil\n}\n\n// SetSuggestionFn sets up suggestions.\nfunc (f *FishBuff) SetSuggestionFn(fn SuggestionFunc) {\n\tf.suggestionFn = fn\n}\n\n// Notify publish suggestions to all listeners.\nfunc (f *FishBuff) Notify(_ bool) {\n\tif f.suggestionFn == nil {\n\t\treturn\n\t}\n\tf.fireSuggestionChanged(f.suggestionFn(string(f.buff)))\n}\n\n// Add adds a new character to the buffer.\nfunc (f *FishBuff) Add(r rune) {\n\tf.CmdBuff.Add(r)\n\tf.Notify(false)\n}\n\n// Delete removes the last character from the buffer.\nfunc (f *FishBuff) Delete() {\n\tf.CmdBuff.Delete()\n\tf.Notify(true)\n}\n\nfunc (f *FishBuff) fireSuggestionChanged(ss []string) {\n\tf.suggestions, f.suggestionIndex = ss, 0\n\n\tvar suggest string\n\tif len(ss) == 0 {\n\t\tf.suggestionIndex = -1\n\t} else {\n\t\tsuggest = ss[f.suggestionIndex]\n\t}\n\tf.SetText(f.GetText(), suggest, true)\n}\n"
  },
  {
    "path": "internal/model/fish_buff_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model_test\n\nimport (\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestFishAdd(t *testing.T) {\n\tm := mockSuggestionListener{}\n\n\tf := model.NewFishBuff(' ', model.FilterBuffer)\n\tf.AddListener(&m)\n\tf.SetSuggestionFn(func(string) sort.StringSlice {\n\t\treturn sort.StringSlice{\"blee\", \"brew\"}\n\t})\n\tf.Add('b')\n\tf.SetActive(true)\n\n\tassert.True(t, m.active)\n\tassert.Equal(t, 1, m.changeCount)\n\tassert.Equal(t, 1, m.suggCount)\n\tassert.Equal(t, \"blee\", m.suggestion)\n\n\tc, ok := f.CurrentSuggestion()\n\tassert.True(t, ok)\n\tassert.Equal(t, \"blee\", c)\n\n\tc, ok = f.NextSuggestion()\n\tassert.True(t, ok)\n\tassert.Equal(t, \"brew\", c)\n\n\tc, ok = f.PrevSuggestion()\n\tassert.True(t, ok)\n\tassert.Equal(t, \"blee\", c)\n}\n\nfunc TestFishDelete(t *testing.T) {\n\tm := mockSuggestionListener{}\n\n\tf := model.NewFishBuff(' ', model.FilterBuffer)\n\tf.AddListener(&m)\n\tf.SetSuggestionFn(func(string) sort.StringSlice {\n\t\treturn sort.StringSlice{\"blee\", \"duh\"}\n\t})\n\tf.Add('a')\n\tf.Delete()\n\tf.SetActive(true)\n\n\tassert.Equal(t, 2, m.changeCount)\n\tassert.Equal(t, 3, m.suggCount)\n\tassert.True(t, m.active)\n\tassert.Equal(t, \"blee\", m.suggestion)\n\n\tc, ok := f.CurrentSuggestion()\n\tassert.True(t, ok)\n\tassert.Equal(t, \"blee\", c)\n\n\tc, ok = f.NextSuggestion()\n\tassert.True(t, ok)\n\tassert.Equal(t, \"duh\", c)\n\n\tc, ok = f.PrevSuggestion()\n\tassert.True(t, ok)\n\tassert.Equal(t, \"blee\", c)\n}\n\n// Helpers...\n\ntype mockSuggestionListener struct {\n\tchangeCount, suggCount int\n\tsuggestion, text       string\n\tactive                 bool\n}\n\nfunc (m *mockSuggestionListener) BufferChanged(_, _ string) {\n\tm.changeCount++\n}\n\nfunc (m *mockSuggestionListener) BufferCompleted(text, suggest string) {\n\tif m.suggestion != suggest {\n\t\tm.suggCount++\n\t}\n\tm.text, m.suggestion = text, suggest\n}\n\nfunc (m *mockSuggestionListener) BufferActive(state bool, _ model.BufferKind) {\n\tm.active = state\n}\n\nfunc (m *mockSuggestionListener) SuggestionChanged(_, sugg string) {\n\tm.suggestion = sugg\n\tm.suggCount++\n}\n"
  },
  {
    "path": "internal/model/flash.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/slogs\"\n)\n\nconst (\n\t// DefaultFlashDelay sets the flash clear delay.\n\tDefaultFlashDelay = 6 * time.Second\n\n\t// FlashInfo represents an info message.\n\tFlashInfo FlashLevel = iota\n\t// FlashWarn represents an warning message.\n\tFlashWarn\n\t// FlashErr represents an error message.\n\tFlashErr\n)\n\n// LevelMessage tracks a message and severity.\ntype LevelMessage struct {\n\tLevel FlashLevel\n\tText  string\n}\n\nfunc newClearMessage() LevelMessage {\n\treturn LevelMessage{}\n}\n\n// IsClear returns true if message is empty.\nfunc (l LevelMessage) IsClear() bool {\n\treturn l.Text == \"\"\n}\n\n// FlashLevel represents flash message severity.\ntype FlashLevel int\n\n// FlashChan represents a flash event channel.\ntype FlashChan chan LevelMessage\n\n// FlashListener represents a text model listener.\ntype FlashListener interface {\n\t// FlashChanged notifies the model changed.\n\tFlashChanged(FlashLevel, string)\n\n\t// FlashCleared notifies when the filter changed.\n\tFlashCleared()\n}\n\n// Flash represents a flash message model.\ntype Flash struct {\n\tmsg     LevelMessage\n\tcancel  context.CancelFunc\n\tdelay   time.Duration\n\tmsgChan chan LevelMessage\n}\n\n// NewFlash returns a new instance.\nfunc NewFlash(dur time.Duration) *Flash {\n\treturn &Flash{\n\t\tdelay:   dur,\n\t\tmsgChan: make(FlashChan, 3),\n\t}\n}\n\n// Channel returns the flash channel.\nfunc (f *Flash) Channel() FlashChan {\n\treturn f.msgChan\n}\n\n// Info displays an info flash message.\nfunc (f *Flash) Info(msg string) {\n\tf.SetMessage(FlashInfo, msg)\n}\n\n// Infof displays a formatted info flash message.\nfunc (f *Flash) Infof(fmat string, args ...any) {\n\tf.Info(fmt.Sprintf(fmat, args...))\n}\n\n// Warn displays a warning flash message.\nfunc (f *Flash) Warn(msg string) {\n\tslog.Warn(msg)\n\tf.SetMessage(FlashWarn, msg)\n}\n\n// Warnf displays a formatted warning flash message.\nfunc (f *Flash) Warnf(fmat string, args ...any) {\n\tf.Warn(fmt.Sprintf(fmat, args...))\n}\n\n// Err displays an error flash message.\nfunc (f *Flash) Err(err error) {\n\tslog.Error(\"Flash error\", slogs.Error, err)\n\tf.SetMessage(FlashErr, err.Error())\n}\n\n// Errf displays a formatted error flash message.\nfunc (f *Flash) Errf(fmat string, args ...any) {\n\tvar err error\n\tfor _, a := range args {\n\t\tif e, ok := a.(error); ok {\n\t\t\terr = e\n\t\t}\n\t}\n\tslog.Error(\"Flash error\",\n\t\tslogs.Error, err,\n\t\tslogs.Message, fmt.Sprintf(fmat, args...),\n\t)\n\tf.SetMessage(FlashErr, fmt.Sprintf(fmat, args...))\n}\n\n// Clear clears the flash message.\nfunc (f *Flash) Clear() {\n\tf.fireCleared()\n}\n\n// SetMessage sets the flash level message.\nfunc (f *Flash) SetMessage(level FlashLevel, msg string) {\n\tif f.cancel != nil {\n\t\tf.cancel()\n\t\tf.cancel = nil\n\t}\n\n\tf.setLevelMessage(LevelMessage{Level: level, Text: msg})\n\tf.fireFlashChanged()\n\n\tctx := context.Background()\n\tctx, f.cancel = context.WithCancel(ctx)\n\tgo f.refresh(ctx)\n}\n\nfunc (f *Flash) refresh(ctx context.Context) {\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-time.After(f.delay):\n\t\t\tf.fireCleared()\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (f *Flash) setLevelMessage(msg LevelMessage) {\n\tf.msg = msg\n}\n\nfunc (f *Flash) fireFlashChanged() {\n\tf.msgChan <- f.msg\n}\n\nfunc (f *Flash) fireCleared() {\n\tf.msgChan <- newClearMessage()\n}\n"
  },
  {
    "path": "internal/model/flash_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model_test\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestFlash(t *testing.T) {\n\tconst delay = 1 * time.Millisecond\n\n\tuu := map[string]struct {\n\t\tlevel model.FlashLevel\n\t\te     string\n\t}{\n\t\t\"info\": {level: model.FlashInfo, e: \"blee\"},\n\t\t\"warn\": {level: model.FlashWarn, e: \"blee\"},\n\t\t\"err\":  {level: model.FlashErr, e: \"blee\"},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tf := model.NewFlash(delay)\n\t\t\tv := newFlash()\n\t\t\tgo v.listen(f.Channel())\n\n\t\t\tswitch u.level {\n\t\t\tcase model.FlashInfo:\n\t\t\t\tf.Info(u.e)\n\t\t\tcase model.FlashWarn:\n\t\t\t\tf.Warn(u.e)\n\t\t\tcase model.FlashErr:\n\t\t\t\tf.Err(errors.New(u.e))\n\t\t\t}\n\n\t\t\ttime.Sleep(5 * delay)\n\t\t\ts, l, m := v.getMetrics()\n\t\t\tassert.Equal(t, 1, s)\n\t\t\tassert.Equal(t, u.level, l)\n\t\t\tassert.Equal(t, u.e, m)\n\t\t})\n\t}\n}\n\nfunc TestFlashBurst(t *testing.T) {\n\tconst delay = 1 * time.Millisecond\n\n\tf := model.NewFlash(delay)\n\tv := newFlash()\n\tgo v.listen(f.Channel())\n\n\tcount := 5\n\tfor i := 1; i <= count; i++ {\n\t\tf.Info(fmt.Sprintf(\"test-%d\", i))\n\t}\n\n\ttime.Sleep(5 * delay)\n\ts, l, m := v.getMetrics()\n\tassert.Equal(t, count, s)\n\tassert.Equal(t, model.FlashInfo, l)\n\tassert.Equal(t, fmt.Sprintf(\"test-%d\", count), m)\n}\n\ntype flash struct {\n\tset, clear int\n\tlevel      model.FlashLevel\n\tmsg        string\n}\n\nfunc newFlash() *flash {\n\treturn &flash{}\n}\n\nfunc (f *flash) getMetrics() (val int, lvl model.FlashLevel, msg string) {\n\treturn f.set, f.level, f.msg\n}\n\nfunc (f *flash) listen(c model.FlashChan) {\n\tfor m := range c {\n\t\tif m.IsClear() {\n\t\t\tf.clear++\n\t\t} else {\n\t\t\tf.set++\n\t\t\tf.level, f.msg = m.Level, m.Text\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/model/helpers.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v4\"\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/sahilm/fuzzy\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nfunc getMeta(ctx context.Context, gvr *client.GVR) (ResourceMeta, error) {\n\tmeta := resourceMeta(gvr)\n\tfactory, ok := ctx.Value(internal.KeyFactory).(dao.Factory)\n\tif !ok {\n\t\treturn ResourceMeta{}, fmt.Errorf(\"expected Factory in context but got %T\", ctx.Value(internal.KeyFactory))\n\t}\n\tmeta.DAO.Init(factory, gvr)\n\n\treturn meta, nil\n}\n\nfunc resourceMeta(gvr *client.GVR) ResourceMeta {\n\tmeta, ok := Registry[gvr]\n\tif !ok {\n\t\tmeta = ResourceMeta{\n\t\t\tDAO:      new(dao.Table),\n\t\t\tRenderer: new(render.Table),\n\t\t}\n\t}\n\tif meta.DAO == nil {\n\t\tmeta.DAO = new(dao.Resource)\n\t}\n\n\treturn meta\n}\n\n// MetaFQN returns a fully qualified resource name.\nfunc MetaFQN(m *metav1.ObjectMeta) string {\n\treturn FQN(m.Namespace, m.Name)\n}\n\n// FQN returns a fully qualified resource name.\nfunc FQN(ns, n string) string {\n\tif ns == \"\" {\n\t\treturn n\n\t}\n\treturn ns + \"/\" + n\n}\n\n// NewExpBackOff returns a new exponential backoff timer.\nfunc NewExpBackOff(ctx context.Context, start, maxVal time.Duration) backoff.BackOffContext {\n\tbf := backoff.NewExponentialBackOff()\n\tbf.InitialInterval, bf.MaxElapsedTime = start, maxVal\n\treturn backoff.WithContext(bf, ctx)\n}\n\nfunc rxFilter(q string, lines []string) fuzzy.Matches {\n\trx, err := regexp.Compile(`(?i)` + q)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tmatches := make(fuzzy.Matches, 0, len(lines))\n\tfor i, l := range lines {\n\t\tlocs := rx.FindAllStringIndex(l, -1)\n\t\tfor _, loc := range locs {\n\t\t\tindexes := make([]int, 0, loc[1]-loc[0])\n\t\t\tfor v := loc[0]; v < loc[1]; v++ {\n\t\t\t\tindexes = append(indexes, v)\n\t\t\t}\n\n\t\t\tmatches = append(matches, fuzzy.Match{Str: q, Index: i, MatchedIndexes: indexes})\n\t\t}\n\t}\n\n\treturn matches\n}\n"
  },
  {
    "path": "internal/model/helpers_int_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"testing\"\n\n\t\"github.com/sahilm/fuzzy\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_rxFilter(t *testing.T) {\n\tuu := map[string]struct {\n\t\tq     string\n\t\tlines []string\n\t\te     fuzzy.Matches\n\t}{\n\t\t\"empty-lines\": {\n\t\t\tq: \"foo\",\n\t\t\te: fuzzy.Matches{},\n\t\t},\n\t\t\"no-match\": {\n\t\t\tq:     \"foo\",\n\t\t\tlines: []string{\"bar\"},\n\t\t\te:     fuzzy.Matches{},\n\t\t},\n\t\t\"single-match\": {\n\t\t\tq:     \"foo\",\n\t\t\tlines: []string{\"foo\", \"bar\", \"baz\"},\n\t\t\te: fuzzy.Matches{\n\t\t\t\t{\n\t\t\t\t\tStr:            \"foo\",\n\t\t\t\t\tIndex:          0,\n\t\t\t\t\tMatchedIndexes: []int{0, 1, 2},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"start-rx-match\": {\n\t\t\tq:     \"(?i)^foo\",\n\t\t\tlines: []string{\"foo\", \"fob\", \"barfoo\"},\n\t\t\te: fuzzy.Matches{\n\t\t\t\t{\n\t\t\t\t\tStr:            \"(?i)^foo\",\n\t\t\t\t\tIndex:          0,\n\t\t\t\t\tMatchedIndexes: []int{0, 1, 2},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"end-rx-match\": {\n\t\t\tq:     \"foo$\",\n\t\t\tlines: []string{\"foo\", \"fob\", \"barfoo\"},\n\t\t\te: fuzzy.Matches{\n\t\t\t\t{\n\t\t\t\t\tStr:            \"foo$\",\n\t\t\t\t\tIndex:          0,\n\t\t\t\t\tMatchedIndexes: []int{0, 1, 2},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStr:            \"foo$\",\n\t\t\t\t\tIndex:          2,\n\t\t\t\t\tMatchedIndexes: []int{3, 4, 5},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"multiple-matches\": {\n\t\t\tq:     \"foo\",\n\t\t\tlines: []string{\"foo\", \"bar\", \"foo bar foo\", \"baz\"},\n\t\t\te: fuzzy.Matches{\n\t\t\t\t{\n\t\t\t\t\tStr:            \"foo\",\n\t\t\t\t\tIndex:          0,\n\t\t\t\t\tMatchedIndexes: []int{0, 1, 2},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStr:            \"foo\",\n\t\t\t\t\tIndex:          2,\n\t\t\t\t\tMatchedIndexes: []int{0, 1, 2},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tStr:            \"foo\",\n\t\t\t\t\tIndex:          2,\n\t\t\t\t\tMatchedIndexes: []int{8, 9, 10},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, rxFilter(u.q, u.lines))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/model/helpers_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/stretchr/testify/assert\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nfunc TestMetaFQN(t *testing.T) {\n\tuu := map[string]struct {\n\t\tmeta metav1.ObjectMeta\n\t\te    string\n\t}{\n\t\t\"all_namespaces\": {\n\t\t\tmeta: metav1.ObjectMeta{Name: \"fred\"},\n\t\t\te:    \"fred\",\n\t\t},\n\t\t\"namespaced\": {\n\t\t\tmeta: metav1.ObjectMeta{Name: \"fred\", Namespace: \"blee\"},\n\t\t\te:    \"blee/fred\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, model.MetaFQN(&u.meta))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/model/hint.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\n// HintListener represents a menu hints listener.\ntype HintListener interface {\n\tHintsChanged(MenuHints)\n}\n\n// Hint represent a hint model.\ntype Hint struct {\n\tdata      MenuHints\n\tlisteners []HintListener\n}\n\n// NewHint return new hint model.\nfunc NewHint() *Hint {\n\treturn &Hint{}\n}\n\n// RemoveListener deletes a listener.\nfunc (h *Hint) RemoveListener(l HintListener) {\n\tvictim := -1\n\tfor i, lis := range h.listeners {\n\t\tif lis == l {\n\t\t\tvictim = i\n\t\t\tbreak\n\t\t}\n\t}\n\tif victim == -1 {\n\t\treturn\n\t}\n\th.listeners = append(h.listeners[:victim], h.listeners[victim+1:]...)\n}\n\n// AddListener adds a hint listener.\nfunc (h *Hint) AddListener(l HintListener) {\n\th.listeners = append(h.listeners, l)\n}\n\n// SetHints set model hints.\nfunc (h *Hint) SetHints(hh MenuHints) {\n\th.data = hh\n\th.fireChanged()\n}\n\n// Peek returns the model data.\nfunc (h *Hint) Peek() MenuHints {\n\treturn h.data\n}\n\nfunc (h *Hint) fireChanged() {\n\tfor _, l := range h.listeners {\n\t\tl.HintsChanged(h.data)\n\t}\n}\n"
  },
  {
    "path": "internal/model/hint_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestHint(t *testing.T) {\n\tuu := map[string]struct {\n\t\thh model.MenuHints\n\t\te  int\n\t}{\n\t\t\"none\": {\n\t\t\tmodel.MenuHints{},\n\t\t\t0,\n\t\t},\n\t\t\"hints\": {\n\t\t\tmodel.MenuHints{\n\t\t\t\t{Mnemonic: \"a\", Description: \"blee\"},\n\t\t\t\t{Mnemonic: \"b\", Description: \"fred\"},\n\t\t\t},\n\t\t\t2,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\th := model.NewHint()\n\t\t\tl := hintL{count: -1}\n\t\t\th.AddListener(&l)\n\t\t\th.SetHints(u.hh)\n\n\t\t\tassert.Equal(t, u.e, l.count)\n\t\t\tassert.Len(t, h.Peek(), u.e)\n\t\t})\n\t}\n}\n\nfunc TestHintRemoveListener(t *testing.T) {\n\th := model.NewHint()\n\tl1, l2, l3 := &hintL{}, &hintL{}, &hintL{}\n\th.AddListener(l1)\n\th.AddListener(l2)\n\n\th.RemoveListener(l2)\n\th.RemoveListener(l3)\n\th.RemoveListener(l1)\n\n\th.SetHints(model.MenuHints{\n\t\tmodel.MenuHint{Mnemonic: \"a\", Description: \"Blee\"},\n\t})\n\n\tassert.Equal(t, 0, l1.count)\n\tassert.Equal(t, 0, l2.count)\n\tassert.Equal(t, 0, l3.count)\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\ntype hintL struct {\n\tcount int\n}\n\nfunc (h *hintL) HintsChanged(hh model.MenuHints) {\n\th.count++\n\th.count += len(hh)\n}\n"
  },
  {
    "path": "internal/model/history.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n)\n\n// MaxHistory tracks max command history.\nconst MaxHistory = 20\n\n// History represents a command history.\ntype History struct {\n\tcommands   []string\n\tlimit      int\n\tcurrentIdx int\n}\n\n// NewHistory returns a new instance.\nfunc NewHistory(limit int) *History {\n\treturn &History{\n\t\tlimit:      limit,\n\t\tcurrentIdx: -1,\n\t}\n}\n\n// List returns the command history.\nfunc (h *History) List() []string {\n\treturn h.commands\n}\n\n// Top returns the last command in the history if present.\nfunc (h *History) Top() (string, bool) {\n\th.currentIdx = len(h.commands) - 1\n\n\treturn h.at(h.currentIdx)\n}\n\nfunc (h *History) SwitchNS(ns string) {\n\tc, ok := h.Top()\n\tif !ok {\n\t\treturn\n\t}\n\ti := cmd.NewInterpreter(c)\n\ti.SwitchNS(ns)\n\tline := i.GetLine()\n\tif _, ok := i.NSArg(); ok && line != c {\n\t\th.Push(line)\n\t\tslog.Debug(\"History (switch-ns)\", slogs.Stack, strings.Join(h.List(), \"|\"))\n\t\treturn\n\t}\n}\n\n// Last returns the nth command prior to last.\nfunc (h *History) Last(idx int) (string, bool) {\n\th.currentIdx = len(h.commands) - idx\n\n\treturn h.at(h.currentIdx)\n}\n\nfunc (h *History) at(idx int) (string, bool) {\n\tif idx < 0 || idx >= len(h.commands) {\n\t\treturn \"\", false\n\t}\n\n\treturn h.commands[idx], true\n}\n\n// Back moves the history position index back by one.\nfunc (h *History) Back() (string, bool) {\n\tif h.Empty() || h.currentIdx <= 0 {\n\t\treturn \"\", false\n\t}\n\th.currentIdx--\n\n\treturn h.at(h.currentIdx)\n}\n\n// Forward moves the history position index forward by one\nfunc (h *History) Forward() (string, bool) {\n\th.currentIdx++\n\tif h.Empty() || h.currentIdx >= len(h.commands) {\n\t\treturn \"\", false\n\t}\n\n\treturn h.at(h.currentIdx)\n}\n\n// Pop removes the single most recent history item\n// and returns a bool if the list changed.\nfunc (h *History) Pop() bool {\n\treturn h.popN(1)\n}\n\n// PopN removes the N most recent history item\n// and returns a bool if the list changed.\n// Argument specifies how many to remove from the history\nfunc (h *History) popN(n int) bool {\n\tpop := len(h.commands) - n\n\tif h.Empty() || pop < 0 {\n\t\treturn false\n\t}\n\th.commands = h.commands[:pop]\n\th.currentIdx = len(h.commands) - 1\n\n\treturn true\n}\n\n// Push adds a new item.\nfunc (h *History) Push(c string) {\n\tif c == \"\" || len(h.commands) >= h.limit {\n\t\treturn\n\t}\n\tif t, ok := h.Top(); ok && t == c {\n\t\treturn\n\t}\n\n\tif h.currentIdx < len(h.commands)-1 {\n\t\th.commands = h.commands[:h.currentIdx+1]\n\t}\n\th.commands = append(h.commands, strings.ToLower(c))\n\th.currentIdx = len(h.commands) - 1\n}\n\n// Clear clears out the stack.\nfunc (h *History) Clear() {\n\th.commands = nil\n\th.currentIdx = -1\n}\n\n// Empty returns true if no history.\nfunc (h *History) Empty() bool {\n\treturn len(h.commands) == 0\n}\n"
  },
  {
    "path": "internal/model/history_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestHistoryClear(t *testing.T) {\n\th := model.NewHistory(3)\n\tfor i := 1; i < 5; i++ {\n\t\th.Push(fmt.Sprintf(\"cmd%d\", i))\n\t}\n\tassert.Equal(t, []string{\"cmd1\", \"cmd2\", \"cmd3\"}, h.List())\n\n\th.Clear()\n\tassert.True(t, h.Empty())\n}\n\nfunc TestHistoryPush(t *testing.T) {\n\th := model.NewHistory(3)\n\tfor i := 1; i < 4; i++ {\n\t\th.Push(fmt.Sprintf(\"cmd%d\", i))\n\t}\n\th.Push(\"cmd1\")\n\th.Push(\"\")\n\n\tassert.Equal(t, []string{\"cmd1\", \"cmd2\", \"cmd3\"}, h.List())\n}\n\nfunc TestHistoryTop(t *testing.T) {\n\tuu := map[string]struct {\n\t\tpush []string\n\t\tpop  int\n\t\tcmd  string\n\t\tok   bool\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"no-one-left\": {\n\t\t\tpush: []string{\"cmd1\", \"cmd2\", \"cmd3\"},\n\t\t\tpop:  3,\n\t\t},\n\n\t\t\"last\": {\n\t\t\tpush: []string{\"cmd1\", \"cmd2\", \"cmd3\"},\n\t\t\tcmd:  \"cmd3\",\n\t\t\tok:   true,\n\t\t},\n\n\t\t\"middle\": {\n\t\t\tpush: []string{\"cmd1\", \"cmd2\", \"cmd3\"},\n\t\t\tpop:  1,\n\t\t\tcmd:  \"cmd2\",\n\t\t\tok:   true,\n\t\t},\n\n\t\t\"first\": {\n\t\t\tpush: []string{\"cmd1\", \"cmd2\", \"cmd3\"},\n\t\t\tpop:  2,\n\t\t\tcmd:  \"cmd1\",\n\t\t\tok:   true,\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\th := model.NewHistory(3)\n\t\t\tfor _, cmd := range u.push {\n\t\t\t\th.Push(cmd)\n\t\t\t}\n\t\t\tfor range u.pop {\n\t\t\t\t_ = h.Pop()\n\t\t\t}\n\n\t\t\tcmd, ok := h.Top()\n\t\t\tassert.Equal(t, u.ok, ok)\n\t\t\tassert.Equal(t, u.cmd, cmd)\n\t\t})\n\t}\n}\n\nfunc TestHistoryBack(t *testing.T) {\n\tuu := map[string]struct {\n\t\tpush []string\n\t\tpop  int\n\t\tcmd  string\n\t\tok   bool\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"pop-all\": {\n\t\t\tpush: []string{\"cmd1\", \"cmd2\", \"cmd3\"},\n\t\t\tpop:  3,\n\t\t},\n\n\t\t\"pop-none\": {\n\t\t\tpush: []string{\"cmd1\", \"cmd2\", \"cmd3\"},\n\t\t\tcmd:  \"cmd2\",\n\t\t\tok:   true,\n\t\t},\n\n\t\t\"pop-one\": {\n\t\t\tpush: []string{\"cmd1\", \"cmd2\", \"cmd3\"},\n\t\t\tpop:  1,\n\t\t\tcmd:  \"cmd1\",\n\t\t\tok:   true,\n\t\t},\n\n\t\t\"pop-to-first\": {\n\t\t\tpush: []string{\"cmd1\", \"cmd2\", \"cmd3\"},\n\t\t\tpop:  2,\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\th := model.NewHistory(3)\n\t\t\tfor _, cmd := range u.push {\n\t\t\t\th.Push(cmd)\n\t\t\t}\n\t\t\tfor range u.pop {\n\t\t\t\t_ = h.Pop()\n\t\t\t}\n\n\t\t\tcmd, ok := h.Back()\n\t\t\tassert.Equal(t, u.ok, ok)\n\t\t\tassert.Equal(t, u.cmd, cmd)\n\t\t})\n\t}\n}\n\nfunc TestHistoryForward(t *testing.T) {\n\tuu := map[string]struct {\n\t\tpush []string\n\t\tback int\n\t\tcmd  string\n\t\tok   bool\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"back-2\": {\n\t\t\tpush: []string{\"cmd1\", \"cmd2\", \"cmd3\"},\n\t\t\tback: 2,\n\t\t\tcmd:  \"cmd2\",\n\t\t\tok:   true,\n\t\t},\n\n\t\t\"back-1\": {\n\t\t\tpush: []string{\"cmd1\", \"cmd2\", \"cmd3\"},\n\t\t\tback: 1,\n\t\t\tcmd:  \"cmd3\",\n\t\t\tok:   true,\n\t\t},\n\n\t\t\"back-all\": {\n\t\t\tpush: []string{\"cmd1\", \"cmd2\", \"cmd3\"},\n\t\t\tback: 3,\n\t\t\tcmd:  \"cmd2\",\n\t\t\tok:   true,\n\t\t},\n\n\t\t\"back-none\": {\n\t\t\tpush: []string{\"cmd1\", \"cmd2\", \"cmd3\"},\n\t\t\tback: 0,\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\th := model.NewHistory(3)\n\t\t\tfor _, cmd := range u.push {\n\t\t\t\th.Push(cmd)\n\t\t\t}\n\t\t\tfor range u.back {\n\t\t\t\t_, _ = h.Back()\n\t\t\t}\n\n\t\t\tcmd, ok := h.Forward()\n\t\t\tassert.Equal(t, u.ok, ok)\n\t\t\tassert.Equal(t, u.cmd, cmd)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/model/log.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/color\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n)\n\n// LogsListener represents a log model listener.\ntype LogsListener interface {\n\t// LogChanged notifies the model changed.\n\tLogChanged([][]byte)\n\n\t// LogCleared indicates logs are cleared.\n\tLogCleared()\n\n\t// LogFailed indicates a log failure.\n\tLogFailed(error)\n\n\t// LogStop indicates logging was canceled.\n\tLogStop()\n\n\t// LogResume indicates logging has resumed.\n\tLogResume()\n\n\t// LogCanceled indicates no more logs will come.\n\tLogCanceled()\n}\n\n// Log represents a resource logger.\ntype Log struct {\n\tfactory      dao.Factory\n\tlines        *dao.LogItems\n\tlisteners    []LogsListener\n\tgvr          *client.GVR\n\tlogOptions   *dao.LogOptions\n\tcancelFn     context.CancelFunc\n\tmx           sync.RWMutex\n\tfilter       string\n\tlastSent     int\n\tflushTimeout time.Duration\n}\n\n// NewLog returns a new model.\nfunc NewLog(gvr *client.GVR, opts *dao.LogOptions, flushTimeout time.Duration) *Log {\n\treturn &Log{\n\t\tgvr:          gvr,\n\t\tlogOptions:   opts,\n\t\tlines:        dao.NewLogItems(),\n\t\tflushTimeout: flushTimeout,\n\t}\n}\n\nfunc (l *Log) GVR() *client.GVR {\n\treturn l.gvr\n}\n\nfunc (l *Log) LogOptions() *dao.LogOptions {\n\treturn l.logOptions\n}\n\n// SinceSeconds returns since seconds option.\nfunc (l *Log) SinceSeconds() int64 {\n\tl.mx.RLock()\n\tdefer l.mx.RUnlock()\n\n\treturn l.logOptions.SinceSeconds\n}\n\n// IsHead returns log head option.\nfunc (l *Log) IsHead() bool {\n\tl.mx.RLock()\n\tdefer l.mx.RUnlock()\n\n\treturn l.logOptions.Head\n}\n\n// ToggleShowTimestamp toggles to logs timestamps.\nfunc (l *Log) ToggleShowTimestamp(b bool) {\n\tl.logOptions.ShowTimestamp = b\n\tl.Refresh()\n}\n\nfunc (l *Log) Head(ctx context.Context) {\n\tl.mx.Lock()\n\tl.logOptions.Head = true\n\tl.mx.Unlock()\n\tl.Restart(ctx)\n}\n\n// SetSinceSeconds sets the logs retrieval time.\nfunc (l *Log) SetSinceSeconds(ctx context.Context, i int64) {\n\tl.logOptions.SinceSeconds, l.logOptions.Head = i, false\n\tl.Restart(ctx)\n}\n\n// Configure sets logger configuration.\nfunc (l *Log) Configure(opts config.Logger) {\n\tl.logOptions.Lines = opts.TailCount\n\tl.logOptions.SinceSeconds = opts.SinceSeconds\n}\n\n// GetPath returns resource path.\nfunc (l *Log) GetPath() string {\n\treturn l.logOptions.Path\n}\n\n// GetContainer returns the resource container if any or \"\" otherwise.\nfunc (l *Log) GetContainer() string {\n\treturn l.logOptions.Container\n}\n\n// HasDefaultContainer returns true if the pod has a default container, false otherwise.\nfunc (l *Log) HasDefaultContainer() bool {\n\treturn l.logOptions.DefaultContainer != \"\"\n}\n\n// Init initializes the model.\nfunc (l *Log) Init(f dao.Factory) {\n\tl.factory = f\n}\n\n// Clear the logs.\nfunc (l *Log) Clear() {\n\tl.mx.Lock()\n\tl.lines.Clear()\n\tl.lastSent = 0\n\tl.mx.Unlock()\n\n\tl.fireLogCleared()\n}\n\n// Refresh refreshes the logs.\nfunc (l *Log) Refresh() {\n\tl.fireLogCleared()\n\tll := make([][]byte, l.lines.Len())\n\tl.lines.Render(0, l.logOptions.ShowTimestamp, ll)\n\tl.fireLogChanged(ll)\n}\n\n// Restart restarts the logger.\nfunc (l *Log) Restart(ctx context.Context) {\n\tl.Stop()\n\tl.Clear()\n\tl.fireLogResume()\n\tl.Start(ctx)\n}\n\n// Start starts logging.\nfunc (l *Log) Start(ctx context.Context) {\n\tif err := l.load(ctx); err != nil {\n\t\tslog.Error(\"Tail logs failed!\", slogs.Error, err)\n\t\tl.fireLogError(err)\n\t}\n}\n\n// Stop terminates logging.\nfunc (l *Log) Stop() {\n\tl.cancel()\n}\n\n// Set sets the log lines (for testing only!)\nfunc (l *Log) Set(lines *dao.LogItems) {\n\tl.mx.Lock()\n\tl.lines.Merge(lines)\n\tl.mx.Unlock()\n\n\tl.fireLogCleared()\n\tll := make([][]byte, l.lines.Len())\n\tl.lines.Render(0, l.logOptions.ShowTimestamp, ll)\n\tl.fireLogChanged(ll)\n}\n\n// ClearFilter resets the log filter if any.\nfunc (l *Log) ClearFilter() {\n\tl.mx.Lock()\n\tl.filter = \"\"\n\tl.mx.Unlock()\n\n\tl.fireLogCleared()\n\tll := make([][]byte, l.lines.Len())\n\tl.lines.Render(0, l.logOptions.ShowTimestamp, ll)\n\tl.fireLogChanged(ll)\n}\n\n// Filter filters the model using either fuzzy or regexp.\nfunc (l *Log) Filter(q string) {\n\tl.mx.Lock()\n\tl.filter = q\n\tl.mx.Unlock()\n\n\tl.fireLogCleared()\n\tl.fireLogBuffChanged(0)\n}\n\nfunc (l *Log) cancel() {\n\tl.mx.Lock()\n\tdefer l.mx.Unlock()\n\tif l.cancelFn != nil {\n\t\tl.cancelFn()\n\t\tl.cancelFn = nil\n\t}\n}\n\nfunc (l *Log) load(ctx context.Context) error {\n\taccessor, err := dao.AccessorFor(l.factory, l.gvr)\n\tif err != nil {\n\t\treturn err\n\t}\n\tloggable, ok := accessor.(dao.Loggable)\n\tif !ok {\n\t\treturn fmt.Errorf(\"resource %s is not Loggable\", l.gvr)\n\t}\n\n\tl.cancel()\n\tctx = context.WithValue(ctx, internal.KeyFactory, l.factory)\n\tctx, l.cancelFn = context.WithCancel(ctx)\n\n\tcc, err := loggable.TailLogs(ctx, l.logOptions)\n\tif err != nil {\n\t\tslog.Error(\"Tail logs failed\", slogs.Error, err)\n\t\tl.cancel()\n\t\tl.fireLogError(err)\n\t}\n\tfor _, c := range cc {\n\t\tgo l.updateLogs(ctx, c)\n\t}\n\n\treturn nil\n}\n\n// Append adds a log line.\nfunc (l *Log) Append(line *dao.LogItem) {\n\tif line == nil || line.IsEmpty() {\n\t\treturn\n\t}\n\tl.mx.Lock()\n\tdefer l.mx.Unlock()\n\tl.logOptions.SinceTime = line.GetTimestamp()\n\tif l.lines.Len() < int(l.logOptions.Lines) {\n\t\tl.lines.Add(line)\n\t\treturn\n\t}\n\tl.lines.Shift(line)\n\tl.lastSent--\n\tif l.lastSent < 0 {\n\t\tl.lastSent = 0\n\t}\n}\n\n// Notify fires of notifications to the listeners.\nfunc (l *Log) Notify() {\n\tl.mx.Lock()\n\tdefer l.mx.Unlock()\n\n\tif l.lastSent < l.lines.Len() {\n\t\tl.fireLogBuffChanged(l.lastSent)\n\t\tl.lastSent = l.lines.Len()\n\t}\n}\n\n// ToggleAllContainers toggles to show all containers logs.\nfunc (l *Log) ToggleAllContainers(ctx context.Context) {\n\tl.logOptions.ToggleAllContainers()\n\tl.Restart(ctx)\n}\n\nfunc (l *Log) updateLogs(ctx context.Context, c dao.LogChan) {\n\tfor {\n\t\tselect {\n\t\tcase item, ok := <-c:\n\t\t\tif !ok {\n\t\t\t\tl.Append(item)\n\t\t\t\tl.Notify()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif item == dao.ItemEOF {\n\t\t\t\tl.fireCanceled()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tl.Append(item)\n\t\t\tvar overflow bool\n\t\t\tl.mx.RLock()\n\t\t\toverflow = int64(l.lines.Len()-l.lastSent) > l.logOptions.Lines\n\t\t\tl.mx.RUnlock()\n\t\t\tif overflow {\n\t\t\t\tl.Notify()\n\t\t\t}\n\t\tcase <-time.After(l.flushTimeout):\n\t\t\tl.Notify()\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// AddListener adds a new model listener.\nfunc (l *Log) AddListener(listener LogsListener) {\n\tl.mx.Lock()\n\tdefer l.mx.Unlock()\n\n\tl.listeners = append(l.listeners, listener)\n}\n\n// RemoveListener delete a listener from the list.\nfunc (l *Log) RemoveListener(listener LogsListener) {\n\tl.mx.Lock()\n\tdefer l.mx.Unlock()\n\n\tvictim := -1\n\tfor i, lis := range l.listeners {\n\t\tif lis == listener {\n\t\t\tvictim = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif victim >= 0 {\n\t\tl.listeners = append(l.listeners[:victim], l.listeners[victim+1:]...)\n\t}\n}\n\nfunc (l *Log) applyFilter(index int, q string) ([][]byte, error) {\n\tif q == \"\" {\n\t\treturn nil, nil\n\t}\n\tmatches, indices, err := l.lines.Filter(index, q, l.logOptions.ShowTimestamp)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// No filter!\n\tif matches == nil {\n\t\tll := make([][]byte, l.lines.Len())\n\t\tl.lines.Render(index, l.logOptions.ShowTimestamp, ll)\n\t\treturn ll, nil\n\t}\n\t// Blank filter\n\tif len(matches) == 0 {\n\t\treturn nil, nil\n\t}\n\tfiltered := make([][]byte, 0, len(matches))\n\tll := make([][]byte, l.lines.Len())\n\tl.lines.Lines(index, l.logOptions.ShowTimestamp, ll)\n\tfor i, idx := range matches {\n\t\tfiltered = append(filtered, color.Highlight(ll[idx], indices[i], 209))\n\t}\n\n\treturn filtered, nil\n}\n\nfunc (l *Log) fireLogBuffChanged(index int) {\n\tll := make([][]byte, l.lines.Len()-index)\n\tif l.filter == \"\" {\n\t\tl.lines.Render(index, l.logOptions.ShowTimestamp, ll)\n\t} else {\n\t\tff, err := l.applyFilter(index, l.filter)\n\t\tif err != nil {\n\t\t\tl.fireLogError(err)\n\t\t\treturn\n\t\t}\n\t\tll = ff\n\t}\n\n\tif len(ll) > 0 {\n\t\tl.fireLogChanged(ll)\n\t}\n}\n\nfunc (l *Log) fireLogResume() {\n\tfor _, lis := range l.listeners {\n\t\tlis.LogResume()\n\t}\n}\n\nfunc (l *Log) fireCanceled() {\n\tfor _, lis := range l.listeners {\n\t\tlis.LogCanceled()\n\t}\n}\n\nfunc (l *Log) fireLogError(err error) {\n\tfor _, lis := range l.listeners {\n\t\tlis.LogFailed(err)\n\t}\n}\n\nfunc (l *Log) fireLogChanged(lines [][]byte) {\n\tfor _, lis := range l.listeners {\n\t\tlis.LogChanged(lines)\n\t}\n}\n\nfunc (l *Log) fireLogCleared() {\n\tvar ll []LogsListener\n\tl.mx.RLock()\n\tll = l.listeners\n\tl.mx.RUnlock()\n\tfor _, lis := range ll {\n\t\tlis.LogCleared()\n\t}\n}\n"
  },
  {
    "path": "internal/model/log_int_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"context\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestUpdateLogs(t *testing.T) {\n\tsize := 100\n\tm := NewLog(client.NewGVR(\"fred\"), makeLogOpts(size), 10*time.Millisecond)\n\tm.Init(makeFactory())\n\n\tv := newMockLogView()\n\tm.AddListener(v)\n\n\tc := make(dao.LogChan, 2)\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tgo m.updateLogs(ctx, c)\n\n\tfor i := range 2 * size {\n\t\tc <- dao.NewLogItemFromString(\"line\" + strconv.Itoa(i))\n\t}\n\ttime.Sleep(2 * time.Second)\n\tassert.Equal(t, size, v.count)\n}\n\nfunc BenchmarkUpdateLogs(b *testing.B) {\n\tsize := 100\n\tm := NewLog(client.NewGVR(\"fred\"), makeLogOpts(size), 10*time.Millisecond)\n\tm.Init(makeFactory())\n\n\tv := newMockLogView()\n\tm.AddListener(v)\n\n\tc := make(dao.LogChan)\n\tgo func() {\n\t\tm.updateLogs(context.Background(), c)\n\t}()\n\titem := dao.NewLogItem([]byte(\"\\033[0;38m2018-12-14T10:36:43.326972-07:00 \\033[0;32mblee line\"))\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor range b.N {\n\t\tc <- item\n\t}\n\tclose(c)\n}\n\n// Helpers...\n\nfunc makeLogOpts(count int) *dao.LogOptions {\n\treturn &dao.LogOptions{\n\t\tPath:      \"fred\",\n\t\tContainer: \"blee\",\n\t\tLines:     int64(count),\n\t}\n}\n\ntype mockLogView struct {\n\tcount int\n}\n\nfunc newMockLogView() *mockLogView {\n\treturn &mockLogView{}\n}\n\nfunc (t *mockLogView) LogChanged(ll [][]byte) {\n\tt.count += len(ll)\n}\nfunc (*mockLogView) LogStop()        {}\nfunc (*mockLogView) LogCanceled()    {}\nfunc (*mockLogView) LogResume()      {}\nfunc (*mockLogView) LogCleared()     {}\nfunc (*mockLogView) LogFailed(error) {}\n"
  },
  {
    "path": "internal/model/log_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/watch\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/client-go/informers\"\n)\n\nfunc init() {\n\tslog.SetDefault(slog.New(slog.DiscardHandler))\n}\n\nfunc TestLogFullBuffer(t *testing.T) {\n\tsize := 4\n\tm := model.NewLog(client.NewGVR(\"fred\"), makeLogOpts(size), 10*time.Millisecond)\n\tm.Init(makeFactory())\n\n\tv := newTestView()\n\tm.AddListener(v)\n\n\tdata := dao.NewLogItems()\n\tfor i := range 2 * size {\n\t\tdata.Add(dao.NewLogItemFromString(\"line\" + strconv.Itoa(i)))\n\t\tm.Append(data.Items()[i])\n\t}\n\tm.Notify()\n\n\tassert.Equal(t, 1, v.dataCalled)\n\tassert.Equal(t, 0, v.clearCalled)\n\tassert.Equal(t, 0, v.errCalled)\n}\n\nfunc TestLogFilter(t *testing.T) {\n\tuu := map[string]struct {\n\t\tq string\n\t\te int\n\t}{\n\t\t\"plain\": {\n\t\t\tq: \"line-1\",\n\t\t\te: 2,\n\t\t},\n\t\t\"regexp\": {\n\t\t\tq: `pod-line-[1-3]{1}`,\n\t\t\te: 4,\n\t\t},\n\t\t\"invert\": {\n\t\t\tq: `!pod-line-1`,\n\t\t\te: 8,\n\t\t},\n\t\t\"fuzzy\": {\n\t\t\tq: `-f po-l1`,\n\t\t\te: 2,\n\t\t},\n\t}\n\n\tsize := 10\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tm := model.NewLog(client.NewGVR(\"fred\"), makeLogOpts(size), 10*time.Millisecond)\n\t\t\tm.Init(makeFactory())\n\n\t\t\tv := newTestView()\n\t\t\tm.AddListener(v)\n\n\t\t\tm.Filter(u.q)\n\t\t\tdata := dao.NewLogItems()\n\t\t\tfor i := range size {\n\t\t\t\tdata.Add(dao.NewLogItemFromString(fmt.Sprintf(\"pod-line-%d\", i+1)))\n\t\t\t\tm.Append(data.Items()[i])\n\t\t\t}\n\n\t\t\tm.Notify()\n\t\t\tassert.Equal(t, 1, v.dataCalled)\n\t\t\tassert.Equal(t, 1, v.clearCalled)\n\t\t\tassert.Equal(t, 0, v.errCalled)\n\t\t\tassert.Len(t, v.data, u.e)\n\n\t\t\tm.ClearFilter()\n\t\t\tassert.Equal(t, 2, v.dataCalled)\n\t\t\tassert.Equal(t, 2, v.clearCalled)\n\t\t\tassert.Equal(t, 0, v.errCalled)\n\t\t\tassert.Len(t, v.data, size)\n\t\t})\n\t}\n}\n\nfunc TestLogStartStop(t *testing.T) {\n\tm := model.NewLog(client.NewGVR(\"fred\"), makeLogOpts(4), 10*time.Millisecond)\n\tm.Init(makeFactory())\n\n\tv := newTestView()\n\tm.AddListener(v)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tm.Start(ctx)\n\tdata := dao.NewLogItems()\n\tdata.Add(dao.NewLogItemFromString(\"line1\"), dao.NewLogItemFromString(\"line2\"))\n\tfor _, d := range data.Items() {\n\t\tm.Append(d)\n\t}\n\tm.Notify()\n\tm.Stop()\n\n\tassert.Equal(t, 1, v.dataCalled)\n\tassert.Equal(t, 0, v.clearCalled)\n\tassert.Equal(t, 1, v.errCalled)\n\tassert.Len(t, v.data, 2)\n}\n\nfunc TestLogClear(t *testing.T) {\n\tm := model.NewLog(client.NewGVR(\"fred\"), makeLogOpts(4), 10*time.Millisecond)\n\tm.Init(makeFactory())\n\tassert.Equal(t, \"fred\", m.GetPath())\n\tassert.Equal(t, \"blee\", m.GetContainer())\n\n\tv := newTestView()\n\tm.AddListener(v)\n\n\tdata := dao.NewLogItems()\n\tdata.Add(dao.NewLogItemFromString(\"line1\"), dao.NewLogItemFromString(\"line2\"))\n\tfor _, d := range data.Items() {\n\t\tm.Append(d)\n\t}\n\tm.Notify()\n\tm.Clear()\n\n\tassert.Equal(t, 1, v.dataCalled)\n\tassert.Equal(t, 1, v.clearCalled)\n\tassert.Equal(t, 0, v.errCalled)\n\tassert.Empty(t, v.data)\n}\n\nfunc TestLogBasic(t *testing.T) {\n\tm := model.NewLog(client.NewGVR(\"fred\"), makeLogOpts(2), 10*time.Millisecond)\n\tm.Init(makeFactory())\n\n\tv := newTestView()\n\tm.AddListener(v)\n\n\tdata := dao.NewLogItems()\n\tdata.Add(dao.NewLogItemFromString(\"line1\"), dao.NewLogItemFromString(\"line2\"))\n\tm.Set(data)\n\n\tassert.Equal(t, 1, v.dataCalled)\n\tassert.Equal(t, 1, v.clearCalled)\n\tassert.Equal(t, 0, v.errCalled)\n\tll := make([][]byte, data.Len())\n\tdata.Lines(0, false, ll)\n\tassert.Equal(t, ll, v.data)\n}\n\nfunc TestLogAppend(t *testing.T) {\n\tm := model.NewLog(client.NewGVR(\"fred\"), makeLogOpts(4), 5*time.Millisecond)\n\tm.Init(makeFactory())\n\n\tv := newTestView()\n\tm.AddListener(v)\n\titems := dao.NewLogItems()\n\titems.Add(dao.NewLogItemFromString(\"blah blah\"))\n\tm.Set(items)\n\tll := make([][]byte, items.Len())\n\titems.Lines(0, false, ll)\n\tassert.Equal(t, ll, v.data)\n\n\tdata := dao.NewLogItems()\n\tdata.Add(\n\t\tdao.NewLogItemFromString(\"line1\"),\n\t\tdao.NewLogItemFromString(\"line2\"),\n\t)\n\tfor _, d := range data.Items() {\n\t\tm.Append(d)\n\t}\n\tassert.Equal(t, 1, v.dataCalled)\n\tll = make([][]byte, items.Len())\n\titems.Lines(0, false, ll)\n\tassert.Equal(t, ll, v.data)\n\n\tm.Notify()\n\tassert.Equal(t, 2, v.dataCalled)\n\tassert.Equal(t, 1, v.clearCalled)\n\tassert.Equal(t, 0, v.errCalled)\n}\n\nfunc TestLogTimedout(t *testing.T) {\n\tm := model.NewLog(client.NewGVR(\"fred\"), makeLogOpts(4), 10*time.Millisecond)\n\tm.Init(makeFactory())\n\n\tv := newTestView()\n\tm.AddListener(v)\n\n\tm.Filter(\"line1\")\n\tdata := dao.NewLogItems()\n\tdata.Add(\n\t\tdao.NewLogItemFromString(\"line1\"),\n\t\tdao.NewLogItemFromString(\"line2\"),\n\t\tdao.NewLogItemFromString(\"line3\"),\n\t\tdao.NewLogItemFromString(\"line4\"),\n\t)\n\tfor _, d := range data.Items() {\n\t\tm.Append(d)\n\t}\n\tm.Notify()\n\tassert.Equal(t, 1, v.dataCalled)\n\tassert.Equal(t, 1, v.clearCalled)\n\tassert.Equal(t, 0, v.errCalled)\n\tconst e = \"\\x1b[38;5;209ml\\x1b[0m\\x1b[38;5;209mi\\x1b[0m\\x1b[38;5;209mn\\x1b[0m\\x1b[38;5;209me\\x1b[0m\\x1b[38;5;209m1\\x1b[0m\"\n\tassert.Equal(t, e, string(v.data[0]))\n}\n\nfunc TestToggleAllContainers(t *testing.T) {\n\topts := makeLogOpts(1)\n\topts.DefaultContainer = \"duh\"\n\tm := model.NewLog(client.NewGVR(\"\"), opts, 10*time.Millisecond)\n\tm.Init(makeFactory())\n\tassert.Equal(t, \"blee\", m.GetContainer())\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tm.ToggleAllContainers(ctx)\n\tassert.Empty(t, m.GetContainer())\n\tm.ToggleAllContainers(ctx)\n\tassert.Equal(t, \"blee\", m.GetContainer())\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc makeLogOpts(count int) *dao.LogOptions {\n\treturn &dao.LogOptions{\n\t\tPath:      \"fred\",\n\t\tContainer: \"blee\",\n\t\tLines:     int64(count),\n\t}\n}\n\n// ----------------------------------------------------------------------------\n\ntype testView struct {\n\tdata        [][]byte\n\tdataCalled  int\n\tclearCalled int\n\terrCalled   int\n}\n\nfunc newTestView() *testView {\n\treturn &testView{}\n}\n\nfunc (*testView) LogCanceled() {}\nfunc (*testView) LogStop()     {}\nfunc (*testView) LogResume()   {}\n\nfunc (t *testView) LogChanged(ll [][]byte) {\n\tt.data = ll\n\tt.dataCalled++\n}\n\nfunc (t *testView) LogCleared() {\n\tt.clearCalled++\n\tt.data = nil\n}\n\nfunc (t *testView) LogFailed(error) {\n\tt.errCalled++\n}\n\n// ----------------------------------------------------------------------------\n\ntype testFactory struct{}\n\nvar _ dao.Factory = testFactory{}\n\nfunc (testFactory) Client() client.Connection {\n\treturn nil\n}\nfunc (testFactory) Get(*client.GVR, string, bool, labels.Selector) (runtime.Object, error) {\n\treturn nil, nil\n}\nfunc (testFactory) List(*client.GVR, string, bool, labels.Selector) ([]runtime.Object, error) {\n\treturn nil, nil\n}\nfunc (testFactory) ForResource(string, *client.GVR) (informers.GenericInformer, error) {\n\treturn nil, nil\n}\nfunc (testFactory) CanForResource(string, *client.GVR, []string) (informers.GenericInformer, error) {\n\treturn nil, nil\n}\nfunc (testFactory) WaitForCacheSync() {}\nfunc (testFactory) Forwarders() watch.Forwarders {\n\treturn nil\n}\nfunc (testFactory) DeleteForwarder(string) {}\n\nfunc makeFactory() dao.Factory {\n\treturn testFactory{}\n}\n"
  },
  {
    "path": "internal/model/menu_hint.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"strconv\"\n)\n\n// MenuHint represents keyboard mnemonic.\ntype MenuHint struct {\n\tMnemonic    string\n\tDescription string\n\tVisible     bool\n}\n\n// IsBlank checks if menu hint is a place holder.\nfunc (m MenuHint) IsBlank() bool {\n\treturn m.Mnemonic == \"\" && m.Description == \"\" && !m.Visible\n}\n\n// String returns a string representation.\nfunc (m MenuHint) String() string {\n\treturn m.Mnemonic\n}\n\n// MenuHints represents a collection of hints.\ntype MenuHints []MenuHint\n\n// Len returns the hints length.\nfunc (h MenuHints) Len() int {\n\treturn len(h)\n}\n\n// Swap swaps to elements.\nfunc (h MenuHints) Swap(i, j int) {\n\th[i], h[j] = h[j], h[i]\n}\n\n// Less returns true if first hint is less than second.\nfunc (h MenuHints) Less(i, j int) bool {\n\tn, err1 := strconv.Atoi(h[i].Mnemonic)\n\tm, err2 := strconv.Atoi(h[j].Mnemonic)\n\tif err1 == nil && err2 == nil {\n\t\treturn n < m\n\t}\n\tif err1 == nil && err2 != nil {\n\t\treturn true\n\t}\n\tif err1 != nil && err2 == nil {\n\t\treturn false\n\t}\n\treturn h[i].Description < h[j].Description\n}\n"
  },
  {
    "path": "internal/model/menu_hint_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model_test\n\nimport (\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestMenuHintsSort(t *testing.T) {\n\tuu := map[string]struct {\n\t\thh model.MenuHints\n\t\te  []int\n\t}{\n\t\t\"mixed\": {\n\t\t\thh: model.MenuHints{\n\t\t\t\tmodel.MenuHint{Mnemonic: \"2\", Description: \"Bubba\"},\n\t\t\t\tmodel.MenuHint{Mnemonic: \"b\", Description: \"Duh\"},\n\t\t\t\tmodel.MenuHint{Mnemonic: \"a\", Description: \"Blee\"},\n\t\t\t\tmodel.MenuHint{Mnemonic: \"1\", Description: \"Zorg\"},\n\t\t\t},\n\t\t\te: []int{3, 0, 2, 1},\n\t\t},\n\t\t\"all_strs\": {\n\t\t\thh: model.MenuHints{\n\t\t\t\tmodel.MenuHint{Mnemonic: \"b\", Description: \"Bob\"},\n\t\t\t\tmodel.MenuHint{Mnemonic: \"a\", Description: \"Abby\"},\n\t\t\t\tmodel.MenuHint{Mnemonic: \"c\", Description: \"Chris\"},\n\t\t\t},\n\t\t\te: []int{1, 0, 2},\n\t\t},\n\t\t\"all_ints\": {\n\t\t\thh: model.MenuHints{\n\t\t\t\tmodel.MenuHint{Mnemonic: \"3\", Description: \"Bob\"},\n\t\t\t\tmodel.MenuHint{Mnemonic: \"2\", Description: \"Abby\"},\n\t\t\t\tmodel.MenuHint{Mnemonic: \"1\", Description: \"Chris\"},\n\t\t\t},\n\t\t\te: []int{2, 1, 0},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\to := make(model.MenuHints, len(u.hh))\n\t\t\tcopy(o, u.hh)\n\t\t\tsort.Sort(u.hh)\n\t\t\tfor i, idx := range u.e {\n\t\t\t\tassert.Equal(t, o[idx], u.hh[i])\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMenuHintBlank(t *testing.T) {\n\tuu := map[string]struct {\n\t\thint model.MenuHint\n\t\te    bool\n\t}{\n\t\t\"yes\": {hint: model.MenuHint{}, e: true},\n\t\t\"no\":  {hint: model.MenuHint{Mnemonic: \"a\", Description: \"blee\"}},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.hint.IsBlank())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/model/pulse.go",
    "content": "package model\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/health\"\n)\n\n// PulseListener represents a health model listener.\ntype PulseListener interface {\n\t// PulseChanged notifies the model data changed.\n\tPulseChanged(*health.Check)\n\n\t// PulseFailed notifies the health check failed.\n\tPulseFailed(error)\n\n\t// MetricsChanged update metrics time series.\n\tMetricsChanged(dao.TimeSeries)\n}\n\n// Pulse tracks multiple resources health.\ntype Pulse struct {\n\tgvr       *client.GVR\n\tnamespace string\n\tlisteners []PulseListener\n\thealth    *PulseHealth\n}\n\n// NewPulse returns a new pulse.\nfunc NewPulse(gvr *client.GVR) *Pulse {\n\treturn &Pulse{\n\t\tgvr: gvr,\n\t}\n}\n\ntype HealthChan chan HealthPoint\n\n// Watch monitors pulses.\nfunc (p *Pulse) Watch(ctx context.Context) (HealthChan, dao.MetricsChan, error) {\n\tf, ok := ctx.Value(internal.KeyFactory).(dao.Factory)\n\tif !ok {\n\t\treturn nil, nil, fmt.Errorf(\"expected Factory in context but got %T\", ctx.Value(internal.KeyFactory))\n\t}\n\tif p.health == nil {\n\t\tp.health = NewPulseHealth(f)\n\t}\n\n\thealthChan := p.health.Watch(ctx, p.namespace)\n\tmetricsChan := dao.DialRecorder(f.Client()).Watch(ctx, p.namespace)\n\n\treturn healthChan, metricsChan, nil\n}\n\n// Refresh update the model now.\nfunc (*Pulse) Refresh(context.Context) {}\n\n// GetNamespace returns the model namespace.\nfunc (p *Pulse) GetNamespace() string {\n\treturn p.namespace\n}\n\n// SetNamespace sets up model namespace.\nfunc (p *Pulse) SetNamespace(ns string) {\n\tif client.IsAllNamespaces(ns) {\n\t\tns = client.BlankNamespace\n\t}\n\tp.namespace = ns\n}\n\n// AddListener adds a listener.\nfunc (p *Pulse) AddListener(l PulseListener) {\n\tp.listeners = append(p.listeners, l)\n}\n\n// RemoveListener delete a listener.\nfunc (p *Pulse) RemoveListener(l PulseListener) {\n\tvictim := -1\n\tfor i, lis := range p.listeners {\n\t\tif lis == l {\n\t\t\tvictim = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif victim >= 0 {\n\t\tp.listeners = append(p.listeners[:victim], p.listeners[victim+1:]...)\n\t}\n}\n"
  },
  {
    "path": "internal/model/pulse_health.go",
    "content": "package model\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nconst pulseRate = 10 * time.Second\n\ntype HealthPoint struct {\n\tGVR           *client.GVR\n\tTotal, Faults int\n}\n\ntype GVRs []*client.GVR\n\nvar PulseGVRs = client.GVRs{\n\tclient.NodeGVR,\n\tclient.NsGVR,\n\tclient.SvcGVR,\n\tclient.EvGVR,\n\n\tclient.PodGVR,\n\tclient.DpGVR,\n\tclient.StsGVR,\n\tclient.DsGVR,\n\n\tclient.JobGVR,\n\tclient.CjGVR,\n\tclient.PvGVR,\n\tclient.PvcGVR,\n\n\tclient.HpaGVR,\n\tclient.IngGVR,\n\tclient.NpGVR,\n\tclient.SaGVR,\n}\n\nfunc (g GVRs) First() *client.GVR {\n\treturn g[0]\n}\n\nfunc (g GVRs) Last() *client.GVR {\n\treturn g[len(g)-1]\n}\n\nfunc (g GVRs) Index(gvr *client.GVR) int {\n\tfor i := range g {\n\t\tif g[i] == gvr {\n\t\t\treturn i\n\t\t}\n\t}\n\n\treturn -1\n}\n\n// PulseHealth tracks resources health.\ntype PulseHealth struct {\n\tfactory dao.Factory\n}\n\n// NewPulseHealth returns a new instance.\nfunc NewPulseHealth(f dao.Factory) *PulseHealth {\n\treturn &PulseHealth{factory: f}\n}\n\nfunc (h *PulseHealth) Watch(ctx context.Context, ns string) HealthChan {\n\tc := make(HealthChan, 2)\n\tctx = context.WithValue(ctx, internal.KeyWithMetrics, false)\n\n\tgo func(ctx context.Context, ns string, c HealthChan) {\n\t\tif err := h.checkPulse(ctx, ns, c); err != nil {\n\t\t\tslog.Error(\"Pulse check failed\", slogs.Error, err)\n\t\t}\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tclose(c)\n\t\t\t\treturn\n\t\t\tcase <-time.After(pulseRate):\n\t\t\t\tif err := h.checkPulse(ctx, ns, c); err != nil {\n\t\t\t\t\tslog.Error(\"Pulse check failed\", slogs.Error, err)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}(ctx, ns, c)\n\n\treturn c\n}\n\nfunc (h *PulseHealth) checkPulse(ctx context.Context, ns string, c HealthChan) error {\n\tslog.Debug(\"Checking pulses...\")\n\tfor _, gvr := range PulseGVRs {\n\t\tcheck, err := h.check(ctx, ns, gvr)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tc <- check\n\t}\n\treturn nil\n}\n\nfunc (h *PulseHealth) check(ctx context.Context, ns string, gvr *client.GVR) (HealthPoint, error) {\n\tmeta, ok := Registry[gvr]\n\tif !ok {\n\t\tmeta = ResourceMeta{\n\t\t\tDAO:      new(dao.Table),\n\t\t\tRenderer: new(render.Table),\n\t\t}\n\t}\n\tif meta.DAO == nil {\n\t\tmeta.DAO = &dao.Resource{}\n\t}\n\n\tmeta.DAO.Init(h.factory, gvr)\n\too, err := meta.DAO.List(ctx, ns)\n\tif err != nil {\n\t\treturn HealthPoint{}, err\n\t}\n\tc := HealthPoint{GVR: gvr, Total: len(oo)}\n\tif isTable(oo) {\n\t\tta := oo[0].(*metav1.Table)\n\t\tc.Total = len(ta.Rows)\n\t\tfor _, row := range ta.Rows {\n\t\t\tif err := meta.Renderer.Healthy(ctx, row); err != nil {\n\t\t\t\tc.Faults++\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor _, o := range oo {\n\t\t\tif err := meta.Renderer.Healthy(ctx, o); err != nil {\n\t\t\t\tc.Faults++\n\t\t\t}\n\t\t}\n\t}\n\tslog.Debug(\"Checked\", slogs.GVR, gvr, slogs.Config, c)\n\n\treturn c, nil\n}\n\nfunc isTable(oo []runtime.Object) bool {\n\tif len(oo) == 0 || len(oo) > 1 {\n\t\treturn false\n\t}\n\t_, ok := oo[0].(*metav1.Table)\n\n\treturn ok\n}\n"
  },
  {
    "path": "internal/model/registry.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/render/helm\"\n\t\"github.com/derailed/k9s/internal/xray\"\n)\n\n// Registry tracks resources metadata.\n// BOZO!! Break up deps and merge into single registrar.\nvar Registry = map[*client.GVR]ResourceMeta{\n\t// Custom...\n\tclient.WkGVR: {\n\t\tDAO:      new(dao.Workload),\n\t\tRenderer: new(render.Workload),\n\t},\n\tclient.RefGVR: {\n\t\tDAO:      new(dao.Reference),\n\t\tRenderer: new(render.Reference),\n\t},\n\tclient.DirGVR: {\n\t\tDAO:      new(dao.Dir),\n\t\tRenderer: new(render.Dir),\n\t},\n\tclient.PuGVR: {\n\t\tDAO: new(dao.Pulse),\n\t},\n\tclient.HmGVR: {\n\t\tDAO:      new(dao.HelmChart),\n\t\tRenderer: new(helm.Chart),\n\t},\n\tclient.HmhGVR: {\n\t\tDAO:      new(dao.HelmHistory),\n\t\tRenderer: new(helm.History),\n\t},\n\tclient.CoGVR: {\n\t\tDAO:          new(dao.Container),\n\t\tRenderer:     new(render.Container),\n\t\tTreeRenderer: new(xray.Container),\n\t},\n\tclient.ScnGVR: {\n\t\tDAO:      new(dao.ImageScan),\n\t\tRenderer: new(render.ImageScan),\n\t},\n\tclient.CtGVR: {\n\t\tDAO:      new(dao.Context),\n\t\tRenderer: new(render.Context),\n\t},\n\tclient.SdGVR: {\n\t\tDAO:      new(dao.ScreenDump),\n\t\tRenderer: new(render.ScreenDump),\n\t},\n\tclient.RbacGVR: {\n\t\tDAO:      new(dao.Rbac),\n\t\tRenderer: new(render.Rbac),\n\t},\n\tclient.PolGVR: {\n\t\tDAO:      new(dao.Policy),\n\t\tRenderer: new(render.Policy),\n\t},\n\tclient.UsrGVR: {\n\t\tDAO:      new(dao.Subject),\n\t\tRenderer: new(render.Subject),\n\t},\n\tclient.GrpGVR: {\n\t\tDAO:      new(dao.Subject),\n\t\tRenderer: new(render.Subject),\n\t},\n\tclient.PfGVR: {\n\t\tDAO:      new(dao.PortForward),\n\t\tRenderer: new(render.PortForward),\n\t},\n\tclient.BeGVR: {\n\t\tDAO:      new(dao.Benchmark),\n\t\tRenderer: new(render.Benchmark),\n\t},\n\tclient.AliGVR: {\n\t\tDAO:      new(dao.Alias),\n\t\tRenderer: new(render.Alias),\n\t},\n\n\t// Discovery...\n\tclient.EpsGVR: {\n\t\tRenderer: new(render.EndpointSlice),\n\t},\n\n\t// Core...\n\tclient.EpGVR: {\n\t\tRenderer: new(render.Endpoints),\n\t},\n\tclient.PodGVR: {\n\t\tDAO:          new(dao.Pod),\n\t\tRenderer:     render.NewPod(),\n\t\tTreeRenderer: new(xray.Pod),\n\t},\n\tclient.NsGVR: {\n\t\tDAO:      new(dao.Namespace),\n\t\tRenderer: new(render.Namespace),\n\t},\n\tclient.SecGVR: {\n\t\tDAO:      new(dao.Secret),\n\t\tRenderer: new(render.Secret),\n\t},\n\tclient.CmGVR: {\n\t\tDAO:      new(dao.ConfigMap),\n\t\tRenderer: new(render.ConfigMap),\n\t},\n\tclient.NodeGVR: {\n\t\tDAO:      new(dao.Node),\n\t\tRenderer: new(render.Node),\n\t},\n\tclient.SvcGVR: {\n\t\tDAO:          new(dao.Service),\n\t\tRenderer:     new(render.Service),\n\t\tTreeRenderer: new(xray.Service),\n\t},\n\tclient.SaGVR: {\n\t\tRenderer: new(render.ServiceAccount),\n\t},\n\tclient.PvGVR: {\n\t\tRenderer: new(render.PersistentVolume),\n\t},\n\tclient.PvcGVR: {\n\t\tRenderer: new(render.PersistentVolumeClaim),\n\t},\n\tclient.EvGVR: {\n\t\tDAO:      new(dao.Table),\n\t\tRenderer: new(render.Event),\n\t},\n\n\t// Apps...\n\tclient.DpGVR: {\n\t\tDAO:          new(dao.Deployment),\n\t\tRenderer:     new(render.Deployment),\n\t\tTreeRenderer: new(xray.Deployment),\n\t},\n\tclient.RsGVR: {\n\t\tRenderer:     new(render.ReplicaSet),\n\t\tTreeRenderer: new(xray.ReplicaSet),\n\t},\n\tclient.StsGVR: {\n\t\tDAO:          new(dao.StatefulSet),\n\t\tRenderer:     new(render.StatefulSet),\n\t\tTreeRenderer: new(xray.StatefulSet),\n\t},\n\tclient.DsGVR: {\n\t\tDAO:          new(dao.DaemonSet),\n\t\tRenderer:     new(render.DaemonSet),\n\t\tTreeRenderer: new(xray.DaemonSet),\n\t},\n\n\t// Extensions...\n\tclient.NpGVR: {\n\t\tRenderer: &render.NetworkPolicy{},\n\t},\n\n\t// Batch...\n\tclient.CjGVR: {\n\t\tDAO:      new(dao.CronJob),\n\t\tRenderer: new(render.CronJob),\n\t},\n\tclient.JobGVR: {\n\t\tDAO:      new(dao.Job),\n\t\tRenderer: new(render.Job),\n\t},\n\n\t// CRDs...\n\tclient.CrdGVR: {\n\t\tDAO:      new(dao.CustomResourceDefinition),\n\t\tRenderer: new(render.CustomResourceDefinition),\n\t},\n\n\t// Storage...\n\tclient.ScGVR: {\n\t\tRenderer: &render.StorageClass{},\n\t},\n\n\t// Policy...\n\tclient.PdbGVR: {\n\t\tRenderer: &render.PodDisruptionBudget{},\n\t},\n\n\t// RBAC...\n\tclient.CrGVR: {\n\t\tDAO:      new(dao.Rbac),\n\t\tRenderer: new(render.ClusterRole),\n\t},\n\tclient.CrbGVR: {\n\t\tRenderer: new(render.ClusterRoleBinding),\n\t},\n\tclient.RoGVR: {\n\t\tRenderer: new(render.Role),\n\t},\n\tclient.RobGVR: {\n\t\tRenderer: new(render.RoleBinding),\n\t},\n}\n"
  },
  {
    "path": "internal/model/rev_values.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\tbackoff \"github.com/cenkalti/backoff/v4\"\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/sahilm/fuzzy\"\n)\n\n// RevValues tracks Helm values representations.\ntype RevValues struct {\n\tgvr       *client.GVR\n\tinUpdate  int32\n\tpath      string\n\trev       string\n\tquery     string\n\tlines     []string\n\tallValues bool\n\tlisteners []ResourceViewerListener\n\toptions   ViewerToggleOpts\n}\n\n// NewRevValues return a new Helm values resource model.\nfunc NewRevValues(gvr *client.GVR, path, rev string) *RevValues {\n\treturn &RevValues{\n\t\tgvr:       gvr,\n\t\tpath:      path,\n\t\trev:       rev,\n\t\tallValues: false,\n\t\tlines:     getRevValues(path, rev),\n\t}\n}\n\nfunc getHelmHistDao() *dao.HelmHistory {\n\treturn Registry[client.HmhGVR].DAO.(*dao.HelmHistory)\n}\n\nfunc getRevValues(path, _ string) []string {\n\tvals, err := getHelmHistDao().GetValues(path, true)\n\tif err != nil {\n\t\tslog.Error(\"Failed to get Helm values\", slogs.Error, err)\n\t}\n\treturn strings.Split(string(vals), \"\\n\")\n}\n\n// GVR returns the resource gvr.\nfunc (v *RevValues) GVR() *client.GVR {\n\treturn v.gvr\n}\n\n// GetPath returns the active resource path.\nfunc (v *RevValues) GetPath() string {\n\treturn v.path\n}\n\n// SetOptions toggle model options.\nfunc (v *RevValues) SetOptions(ctx context.Context, opts ViewerToggleOpts) {\n\tv.options = opts\n\tif err := v.refresh(ctx); err != nil {\n\t\tv.fireResourceFailed(err)\n\t}\n}\n\n// Filter filters the model.\nfunc (v *RevValues) Filter(q string) {\n\tv.query = q\n\tv.filterChanged(v.lines)\n}\n\nfunc (v *RevValues) filterChanged(lines []string) {\n\tv.fireResourceChanged(lines, v.filter(v.query, lines))\n}\n\nfunc (v *RevValues) filter(q string, lines []string) fuzzy.Matches {\n\tif q == \"\" {\n\t\treturn nil\n\t}\n\tif f, ok := internal.IsFuzzySelector(q); ok {\n\t\treturn v.fuzzyFilter(strings.TrimSpace(f), lines)\n\t}\n\treturn rxFilter(q, lines)\n}\n\nfunc (*RevValues) fuzzyFilter(q string, lines []string) fuzzy.Matches {\n\treturn fuzzy.Find(q, lines)\n}\n\nfunc (v *RevValues) fireResourceChanged(lines []string, matches fuzzy.Matches) {\n\tfor _, l := range v.listeners {\n\t\tl.ResourceChanged(lines, matches)\n\t}\n}\n\nfunc (v *RevValues) fireResourceFailed(err error) {\n\tfor _, l := range v.listeners {\n\t\tl.ResourceFailed(err)\n\t}\n}\n\n// ClearFilter clear out the filter.\nfunc (v *RevValues) ClearFilter() {\n\tv.query = \"\"\n}\n\n// Peek returns the current model data.\nfunc (v *RevValues) Peek() []string {\n\treturn v.lines\n}\n\n// Refresh updates model data.\nfunc (v *RevValues) Refresh(ctx context.Context) error {\n\treturn v.refresh(ctx)\n}\n\n// Watch watches for Values changes.\nfunc (v *RevValues) Watch(ctx context.Context) error {\n\tif err := v.refresh(ctx); err != nil {\n\t\treturn err\n\t}\n\tgo v.updater(ctx)\n\n\treturn nil\n}\n\nfunc (v *RevValues) updater(ctx context.Context) {\n\tdefer slog.Debug(\"YAML canceled\", slogs.GVR, v.gvr)\n\n\tbackOff := NewExpBackOff(ctx, defaultReaderRefreshRate, maxReaderRetryInterval)\n\tdelay := defaultReaderRefreshRate\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-time.After(delay):\n\t\t\tif err := v.refresh(ctx); err != nil {\n\t\t\t\tv.fireResourceFailed(err)\n\t\t\t\tif delay = backOff.NextBackOff(); delay == backoff.Stop {\n\t\t\t\t\tslog.Error(\"Giving up retrieving chart values\", slogs.Error, err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tbackOff.Reset()\n\t\t\t\tdelay = defaultReaderRefreshRate\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (v *RevValues) refresh(context.Context) error {\n\tif !atomic.CompareAndSwapInt32(&v.inUpdate, 0, 1) {\n\t\tslog.Debug(\"Dropping update...\")\n\t\treturn errors.New(\"refresh in progress, dropping\")\n\t}\n\tdefer atomic.StoreInt32(&v.inUpdate, 0)\n\n\tv.reconcile()\n\n\treturn nil\n}\n\nfunc (v *RevValues) reconcile() {\n\tv.fireResourceChanged(v.lines, v.filter(v.query, v.lines))\n}\n\n// AddListener adds a new model listener.\nfunc (v *RevValues) AddListener(l ResourceViewerListener) {\n\tv.listeners = append(v.listeners, l)\n}\n\n// RemoveListener delete a listener from the list.\nfunc (v *RevValues) RemoveListener(l ResourceViewerListener) {\n\tvictim := -1\n\tfor i, lis := range v.listeners {\n\t\tif lis == l {\n\t\t\tvictim = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif victim >= 0 {\n\t\tv.listeners = append(v.listeners[:victim], v.listeners[victim+1:]...)\n\t}\n}\n"
  },
  {
    "path": "internal/model/semver.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strconv\"\n)\n\nvar versionRX = regexp.MustCompile(`\\Av(\\d+)\\.(\\d+)\\.(\\d+)\\z`)\n\n// SemVer represents a semantic version.\ntype SemVer struct {\n\tMajor, Minor, Patch int\n}\n\n// NewSemVer returns a new semantic version.\nfunc NewSemVer(version string) *SemVer {\n\tvar v SemVer\n\tv.Major, v.Minor, v.Patch = v.parse(NormalizeVersion(version))\n\n\treturn &v\n}\n\n// String returns version as a string.\nfunc (v *SemVer) String() string {\n\treturn fmt.Sprintf(\"v%d.%d.%d\", v.Major, v.Minor, v.Patch)\n}\n\nfunc (*SemVer) parse(version string) (major, minor, patch int) {\n\tmm := versionRX.FindStringSubmatch(version)\n\tif len(mm) < 4 {\n\t\treturn\n\t}\n\tmajor, _ = strconv.Atoi(mm[1])\n\tminor, _ = strconv.Atoi(mm[2])\n\tpatch, _ = strconv.Atoi(mm[3])\n\n\treturn\n}\n\n// NormalizeVersion ensures the version starts with a v.\nfunc NormalizeVersion(version string) string {\n\tif version == \"\" {\n\t\treturn version\n\t}\n\tif version[0] == 'v' {\n\t\treturn version\n\t}\n\treturn \"v\" + version\n}\n\n// IsCurrent asserts if at latest release.\nfunc (v *SemVer) IsCurrent(latest *SemVer) bool {\n\treturn v.Major >= latest.Major && v.Minor >= latest.Minor && v.Patch >= latest.Patch\n}\n"
  },
  {
    "path": "internal/model/semver_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNewSemVer(t *testing.T) {\n\tuu := map[string]struct {\n\t\tversion             string\n\t\tmajor, minor, patch int\n\t}{\n\t\t\"plain\": {\n\t\t\tversion: \"0.11.1\",\n\t\t\tmajor:   0,\n\t\t\tminor:   11,\n\t\t\tpatch:   1,\n\t\t},\n\t\t\"normalized\": {\n\t\t\tversion: \"v10.11.12\",\n\t\t\tmajor:   10,\n\t\t\tminor:   11,\n\t\t\tpatch:   12,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tv := model.NewSemVer(u.version)\n\t\t\tassert.Equal(t, u.major, v.Major)\n\t\t\tassert.Equal(t, u.minor, v.Minor)\n\t\t\tassert.Equal(t, u.patch, v.Patch)\n\t\t})\n\t}\n}\n\nfunc TestSemVerIsCurrent(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcurrent, latest string\n\t\te               bool\n\t}{\n\t\t\"same\": {\n\t\t\tcurrent: \"0.11.1\",\n\t\t\tlatest:  \"0.11.1\",\n\t\t\te:       true,\n\t\t},\n\t\t\"older\": {\n\t\t\tcurrent: \"v10.11.12\",\n\t\t\tlatest:  \"v10.11.13\",\n\t\t},\n\t\t\"newer\": {\n\t\t\tcurrent: \"10.11.13\",\n\t\t\tlatest:  \"10.11.12\",\n\t\t\te:       true,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tv1, v2 := model.NewSemVer(u.current), model.NewSemVer(u.latest)\n\t\t\tassert.Equal(t, u.e, v1.IsCurrent(v2))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/model/stack.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sync\"\n\n\t\"github.com/derailed/k9s/internal/slogs\"\n)\n\nconst (\n\t// StackPush denotes an add on the stack.\n\tStackPush StackAction = 1 << iota\n\n\t// StackPop denotes a delete on the stack.\n\tStackPop\n)\n\n// StackAction represents an action on the stack.\ntype StackAction int\n\n// StackEvent represents an operation on a view stack.\ntype StackEvent struct {\n\t// Kind represents the event condition.\n\tAction StackAction\n\n\t// Item represents the targeted item.\n\tComponent Component\n}\n\n// StackListener represents a stack listener.\ntype StackListener interface {\n\t// StackPushed indicates a new item was added.\n\tStackPushed(Component)\n\n\t// StackPopped indicates an item was deleted\n\tStackPopped(old, new Component)\n\n\t// StackTop indicates the top of the stack\n\tStackTop(Component)\n}\n\n// Stack represents a stacks of components.\ntype Stack struct {\n\tcomponents []Component\n\tlisteners  []StackListener\n\tmx         sync.RWMutex\n}\n\n// NewStack returns a new initialized stack.\nfunc NewStack() *Stack {\n\treturn &Stack{}\n}\n\n// Flatten returns a string representation of the component stack.\nfunc (s *Stack) Flatten() []string {\n\ts.mx.RLock()\n\tdefer s.mx.RUnlock()\n\n\tss := make([]string, len(s.components))\n\tfor i, c := range s.components {\n\t\tss[i] = c.Name()\n\t}\n\treturn ss\n}\n\n// RemoveListener removes a listener.\nfunc (s *Stack) RemoveListener(l StackListener) {\n\tvictim := -1\n\tfor i, lis := range s.listeners {\n\t\tif lis == l {\n\t\t\tvictim = i\n\t\t\tbreak\n\t\t}\n\t}\n\tif victim == -1 {\n\t\treturn\n\t}\n\ts.listeners = append(s.listeners[:victim], s.listeners[victim+1:]...)\n}\n\n// AddListener registers a stack listener.\nfunc (s *Stack) AddListener(l StackListener) {\n\ts.listeners = append(s.listeners, l)\n\tif !s.Empty() {\n\t\tl.StackTop(s.Top())\n\t}\n}\n\n// Push adds a new item.\nfunc (s *Stack) Push(c Component) {\n\tif top := s.Top(); top != nil {\n\t\ttop.Stop()\n\t}\n\n\ts.mx.Lock()\n\ts.components = append(s.components, c)\n\ts.mx.Unlock()\n\ts.notify(StackPush, c)\n}\n\n// Pop removed the top item and returns it.\nfunc (s *Stack) Pop() (Component, bool) {\n\tif s.Empty() {\n\t\treturn nil, false\n\t}\n\n\tvar c Component\n\ts.mx.Lock()\n\tc = s.components[len(s.components)-1]\n\tc.Stop()\n\ts.components = s.components[:len(s.components)-1]\n\ts.mx.Unlock()\n\n\ts.notify(StackPop, c)\n\n\treturn c, true\n}\n\n// Peek returns stack state.\nfunc (s *Stack) Peek() []Component {\n\ts.mx.RLock()\n\tdefer s.mx.RUnlock()\n\n\treturn s.components\n}\n\n// Clear clear out the stack using pops.\nfunc (s *Stack) Clear() {\n\tfor range s.components {\n\t\ts.Pop()\n\t}\n}\n\n// Empty returns true if the stack is empty.\nfunc (s *Stack) Empty() bool {\n\ts.mx.RLock()\n\tdefer s.mx.RUnlock()\n\n\treturn len(s.components) == 0\n}\n\n// IsLast indicates if stack only has one item left.\nfunc (s *Stack) IsLast() bool {\n\treturn len(s.components) == 1\n}\n\n// Previous returns the previous component if any.\nfunc (s *Stack) Previous() Component {\n\tif s.Empty() {\n\t\treturn nil\n\t}\n\tif s.IsLast() {\n\t\treturn s.Top()\n\t}\n\n\treturn s.components[len(s.components)-2]\n}\n\n// Top returns the top most item or nil if the stack is empty.\nfunc (s *Stack) Top() Component {\n\tif s.Empty() {\n\t\treturn nil\n\t}\n\n\ts.mx.RLock()\n\tdefer s.mx.RUnlock()\n\treturn s.components[len(s.components)-1]\n}\n\nfunc (s *Stack) notify(a StackAction, c Component) {\n\tfor _, l := range s.listeners {\n\t\tswitch a {\n\t\tcase StackPush:\n\t\t\tl.StackPushed(c)\n\t\tcase StackPop:\n\t\t\tl.StackPopped(c, s.Top())\n\t\t}\n\t}\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\n// Dump prints out the stack.\nfunc (s *Stack) Dump() {\n\tslog.Debug(\"Stack Dump\", slogs.Stack, fmt.Sprintf(\"%p\", s))\n\tfor i, c := range s.components {\n\t\tslog.Debug(fmt.Sprintf(\"%d -- %s -- %#v\", i, c.Name(), c))\n\t}\n\tslog.Debug(\"------------------\")\n}\n"
  },
  {
    "path": "internal/model/stack_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model_test\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n)\n\nfunc init() {\n\tslog.SetDefault(slog.New(slog.DiscardHandler))\n}\n\nfunc TestStackClear(t *testing.T) {\n\tcomps := []model.Component{makeC(\"c1\"), makeC(\"c2\"), makeC(\"c3\")}\n\tuu := map[string]struct {\n\t\titems []model.Component\n\t}{\n\t\t\"empty\": {\n\t\t\titems: []model.Component{},\n\t\t},\n\t\t\"items\": {\n\t\t\titems: comps,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\ts := model.NewStack()\n\t\t\tfor _, c := range u.items {\n\t\t\t\ts.Push(c)\n\t\t\t}\n\t\t\ts.Clear()\n\t\t\tassert.True(t, s.Empty())\n\t\t})\n\t}\n}\n\nfunc TestStackPrevious(t *testing.T) {\n\tcomps := []model.Component{makeC(\"c1\"), makeC(\"c2\"), makeC(\"c3\")}\n\tuu := map[string]struct {\n\t\titems []model.Component\n\t\tpops  int\n\t\te     model.Component\n\t}{\n\t\t\"empty\": {\n\t\t\titems: []model.Component{},\n\t\t\tpops:  0,\n\t\t\te:     nil,\n\t\t},\n\t\t\"one_left\": {\n\t\t\titems: comps,\n\t\t\tpops:  1,\n\t\t\te:     comps[0],\n\t\t},\n\t\t\"none_left\": {\n\t\t\titems: comps,\n\t\t\tpops:  2,\n\t\t\te:     comps[0],\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\ts := model.NewStack()\n\t\t\tfor _, c := range u.items {\n\t\t\t\ts.Push(c)\n\t\t\t}\n\t\t\tfor range u.pops {\n\t\t\t\ts.Pop()\n\t\t\t}\n\t\t\tassert.Equal(t, u.e, s.Previous())\n\t\t})\n\t}\n}\n\nfunc TestStackIsLast(t *testing.T) {\n\tuu := map[string]struct {\n\t\titems []model.Component\n\t\tpops  int\n\t\te     bool\n\t}{\n\t\t\"empty\": {\n\t\t\titems: []model.Component{},\n\t\t},\n\t\t\"normal\": {\n\t\t\titems: []model.Component{makeC(\"c1\"), makeC(\"c2\"), makeC(\"c3\")},\n\t\t\tpops:  1,\n\t\t},\n\t\t\"last\": {\n\t\t\titems: []model.Component{makeC(\"c1\"), makeC(\"c2\"), makeC(\"c3\")},\n\t\t\tpops:  2,\n\t\t\te:     true,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\ts := model.NewStack()\n\t\t\tfor _, c := range u.items {\n\t\t\t\ts.Push(c)\n\t\t\t}\n\t\t\tfor range u.pops {\n\t\t\t\ts.Pop()\n\t\t\t}\n\t\t\tassert.Equal(t, u.e, s.IsLast())\n\t\t})\n\t}\n}\n\nfunc TestStackFlatten(t *testing.T) {\n\tuu := map[string]struct {\n\t\titems []model.Component\n\t\te     []string\n\t}{\n\t\t\"empty\": {\n\t\t\titems: []model.Component{},\n\t\t\te:     []string{},\n\t\t},\n\t\t\"normal\": {\n\t\t\titems: []model.Component{makeC(\"c1\"), makeC(\"c2\"), makeC(\"c3\")},\n\t\t\te:     []string{\"c1\", \"c2\", \"c3\"},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\ts := model.NewStack()\n\t\t\tfor _, c := range u.items {\n\t\t\t\ts.Push(c)\n\t\t\t}\n\t\t\tassert.Equal(t, u.e, s.Flatten())\n\t\t\tassert.Len(t, s.Peek(), len(u.e))\n\t\t})\n\t}\n}\n\nfunc TestStackPush(t *testing.T) {\n\ttop := c{}\n\tuu := map[string]struct {\n\t\titems []model.Component\n\t\tpop   int\n\t\te     bool\n\t\ttop   model.Component\n\t}{\n\t\t\"empty\": {\n\t\t\titems: []model.Component{},\n\t\t\tpop:   3,\n\t\t\te:     true,\n\t\t},\n\t\t\"full\": {\n\t\t\titems: []model.Component{c{}, c{}, top},\n\t\t\tpop:   3,\n\t\t\te:     true,\n\t\t},\n\t\t\"pop\": {\n\t\t\titems: []model.Component{c{}, c{}, top},\n\t\t\tpop:   2,\n\t\t\te:     false,\n\t\t\ttop:   top,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\ts := model.NewStack()\n\t\t\tfor _, c := range u.items {\n\t\t\t\ts.Push(c)\n\t\t\t}\n\t\t\tfor range u.pop {\n\t\t\t\ts.Pop()\n\t\t\t}\n\t\t\tassert.Equal(t, u.e, s.Empty())\n\t\t\tif !u.e {\n\t\t\t\tassert.Equal(t, u.top, s.Top())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestStackTop(t *testing.T) {\n\ttop := c{}\n\tuu := map[string]struct {\n\t\titems []model.Component\n\t\te     model.Component\n\t}{\n\t\t\"blank\": {\n\t\t\titems: []model.Component{},\n\t\t},\n\t\t\"push3\": {\n\t\t\titems: []model.Component{c{}, c{}, top},\n\t\t\te:     top,\n\t\t},\n\t\t\"push1\": {\n\t\t\titems: []model.Component{top},\n\t\t\te:     top,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\ts := model.NewStack()\n\t\t\tfor _, item := range u.items {\n\t\t\t\ts.Push(item)\n\t\t\t}\n\t\t\tv := s.Top()\n\t\t\tassert.Equal(t, u.e, v)\n\t\t})\n\t}\n}\n\nfunc TestStackAddListener(t *testing.T) {\n\titems := []model.Component{c{}, c{}, c{}}\n\ts := model.NewStack()\n\tl := stackL{}\n\ts.AddListener(&l)\n\tfor _, item := range items {\n\t\ts.Push(item)\n\t}\n\tassert.Equal(t, 3, l.count)\n\n\tfor range items {\n\t\ts.Pop()\n\t}\n\tassert.Equal(t, 0, l.count)\n}\n\nfunc TestStackAddListenerAfter(t *testing.T) {\n\titems := []model.Component{c{}, c{}, c{}}\n\ts := model.NewStack()\n\tl := stackL{}\n\tfor _, item := range items {\n\t\ts.Push(item)\n\t}\n\ts.AddListener(&l)\n\tassert.Equal(t, 1, l.tops)\n\tassert.Equal(t, 0, l.count)\n}\n\nfunc TestStackRemoveListener(t *testing.T) {\n\ts := model.NewStack()\n\tl1, l2, l3 := &stackL{}, &stackL{}, &stackL{}\n\ts.AddListener(l1)\n\ts.AddListener(l2)\n\n\ts.RemoveListener(l2)\n\ts.RemoveListener(l3)\n\ts.RemoveListener(l1)\n\n\ts.Push(c{})\n\n\tassert.Equal(t, 0, l1.count)\n\tassert.Equal(t, 0, l2.count)\n\tassert.Equal(t, 0, l3.count)\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\ntype stackL struct {\n\tcount, tops int\n}\n\nfunc (s *stackL) StackPushed(model.Component) {\n\ts.count++\n}\n\nfunc (s *stackL) StackPopped(_, _ model.Component) {\n\ts.count--\n}\n\nfunc (s *stackL) StackTop(model.Component) { s.tops++ }\n\ntype c struct {\n\tname string\n}\n\nfunc makeC(n string) c {\n\treturn c{name: n}\n}\n\nfunc (c) InCmdMode() bool                                            { return false }\nfunc (c c) Name() string                                             { return c.name }\nfunc (c) SetCommand(*cmd.Interpreter)                                {}\nfunc (c) Hints() model.MenuHints                                     { return nil }\nfunc (c) HasFocus() bool                                             { return false }\nfunc (c) ExtraHints() map[string]string                              { return nil }\nfunc (c) Draw(tcell.Screen)                                          {}\nfunc (c) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { return nil }\nfunc (c) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) {\n\treturn nil\n}\nfunc (c) SetRect(int, int, int, int)             {}\nfunc (c) GetRect() (a, b, c, d int)              { return 0, 0, 0, 0 }\nfunc (c) GetFocusable() tview.Focusable          { return nil }\nfunc (c) Focus(func(tview.Primitive))            {}\nfunc (c) Blur()                                  {}\nfunc (c) Start()                                 {}\nfunc (c) Stop()                                  {}\nfunc (c) Init(context.Context) error             { return nil }\nfunc (c) SetFilter(string, bool)                 {}\nfunc (c) SetLabelSelector(labels.Selector, bool) {}\n"
  },
  {
    "path": "internal/model/table.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\tbackoff \"github.com/cenkalti/backoff/v4\"\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nconst initRefreshRate = 300 * time.Millisecond\n\n// TableListener represents a table model listener.\ntype TableListener interface {\n\t// TableNoData notifies listener no data was found.\n\tTableNoData(*model1.TableData)\n\n\t// TableDataChanged notifies the model data changed.\n\tTableDataChanged(*model1.TableData)\n\n\t// TableLoadFailed notifies the load failed.\n\tTableLoadFailed(error)\n}\n\n// Table represents a table model.\ntype Table struct {\n\tgvr           *client.GVR\n\tdata          *model1.TableData\n\tlisteners     []TableListener\n\tinUpdate      int32\n\trefreshRate   time.Duration\n\tinstance      string\n\tlabelSelector labels.Selector\n\tmx            sync.RWMutex\n\tvs            *config.ViewSetting\n}\n\n// NewTable returns a new table model.\nfunc NewTable(gvr *client.GVR) *Table {\n\treturn &Table{\n\t\tgvr:         gvr,\n\t\tdata:        model1.NewTableData(gvr),\n\t\trefreshRate: 2 * time.Second,\n\t}\n}\n\nfunc (t *Table) SetViewSetting(ctx context.Context, vs *config.ViewSetting) {\n\tt.mx.Lock()\n\tt.vs = vs\n\tt.mx.Unlock()\n\n\tif ctx != context.Background() {\n\t\tif err := t.reconcile(ctx); err != nil {\n\t\t\tslog.Error(\"Refresh failed\", slogs.GVR, t.gvr)\n\t\t}\n\t}\n}\n\n// SetLabelSelector sets the labels selector.\nfunc (t *Table) SetLabelSelector(sel labels.Selector) {\n\tt.mx.Lock()\n\tdefer t.mx.Unlock()\n\n\tt.labelSelector = sel\n}\n\n// GetLabelSelector sets the labels selector.\nfunc (t *Table) GetLabelSelector() labels.Selector {\n\tt.mx.Lock()\n\tdefer t.mx.Unlock()\n\n\treturn t.labelSelector\n}\n\n// SetInstance sets a single entry table.\nfunc (t *Table) SetInstance(path string) {\n\tt.instance = path\n}\n\n// AddListener adds a new model listener.\nfunc (t *Table) AddListener(l TableListener) {\n\tt.mx.Lock()\n\tdefer t.mx.Unlock()\n\n\tt.listeners = append(t.listeners, l)\n}\n\n// RemoveListener delete a listener from the list.\nfunc (t *Table) RemoveListener(l TableListener) {\n\tvictim := -1\n\tfor i, lis := range t.listeners {\n\t\tif lis == l {\n\t\t\tvictim = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif victim >= 0 {\n\t\tt.mx.Lock()\n\t\tt.listeners = append(t.listeners[:victim], t.listeners[victim+1:]...)\n\t\tt.mx.Unlock()\n\t}\n}\n\n// Watch initiates model updates.\nfunc (t *Table) Watch(ctx context.Context) error {\n\tif err := t.refresh(ctx); err != nil {\n\t\treturn err\n\t}\n\tgo t.updater(ctx)\n\n\treturn nil\n}\n\n// Refresh updates the table content.\nfunc (t *Table) Refresh(ctx context.Context) error {\n\treturn t.refresh(ctx)\n}\n\n// Get returns a resource instance if found, else an error.\nfunc (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) {\n\tmeta, err := getMeta(ctx, t.gvr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn meta.DAO.Get(ctx, path)\n}\n\n// Delete deletes a resource.\nfunc (t *Table) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace dao.Grace) error {\n\tmeta, err := getMeta(ctx, t.gvr)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnuker, ok := meta.DAO.(dao.Nuker)\n\tif !ok {\n\t\treturn fmt.Errorf(\"no nuker for %q\", meta.DAO.GVR())\n\t}\n\n\treturn nuker.Delete(ctx, path, propagation, grace)\n}\n\n// GetNamespace returns the model namespace.\nfunc (t *Table) GetNamespace() string {\n\treturn t.data.GetNamespace()\n}\n\n// SetNamespace sets up model namespace.\nfunc (t *Table) SetNamespace(ns string) {\n\tt.data.Reset(ns)\n}\n\n// InNamespace checks if current namespace matches desired namespace.\nfunc (t *Table) InNamespace(ns string) bool {\n\treturn t.data.GetNamespace() == ns && !t.data.Empty()\n}\n\n// SetRefreshRate sets model refresh duration.\nfunc (t *Table) SetRefreshRate(d time.Duration) {\n\tt.refreshRate = d\n}\n\n// ClusterWide checks if resource is scope for all namespaces.\nfunc (t *Table) ClusterWide() bool {\n\treturn client.IsClusterWide(t.data.GetNamespace())\n}\n\n// Empty returns true if no model data.\nfunc (t *Table) Empty() bool {\n\treturn t.data.Empty()\n}\n\n// RowCount returns the row count.\nfunc (t *Table) RowCount() int {\n\treturn t.data.RowCount()\n}\n\n// Peek returns model data.\nfunc (t *Table) Peek() *model1.TableData {\n\tt.mx.RLock()\n\tdefer t.mx.RUnlock()\n\n\treturn t.data.Clone()\n}\n\nfunc (t *Table) updater(ctx context.Context) {\n\tbf := backoff.NewExponentialBackOff()\n\tbf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxReaderRetryInterval\n\trate := initRefreshRate\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-time.After(rate):\n\t\t\trate = t.refreshRate\n\t\t\terr := backoff.Retry(func() error {\n\t\t\t\tif err := t.refresh(ctx); err != nil {\n\t\t\t\t\tslog.Error(\"Refresh failed\", slogs.GVR, t.gvr)\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t}, backoff.WithContext(bf, ctx))\n\t\t\tif err != nil {\n\t\t\t\tslog.Warn(\"Reconciler exited\", slogs.Error, err)\n\t\t\t\tt.fireTableLoadFailed(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (t *Table) refresh(ctx context.Context) error {\n\tif !atomic.CompareAndSwapInt32(&t.inUpdate, 0, 1) {\n\t\tslog.Debug(\"Dropping update...\")\n\t\treturn nil\n\t}\n\tdefer atomic.StoreInt32(&t.inUpdate, 0)\n\n\tif err := t.reconcile(ctx); err != nil {\n\t\treturn err\n\t}\n\tdata := t.Peek()\n\tif data.RowCount() == 0 {\n\t\tt.fireNoData(data)\n\t} else {\n\t\tt.fireTableChanged(data)\n\t}\n\n\treturn nil\n}\n\nfunc (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, error) {\n\tfactory, ok := ctx.Value(internal.KeyFactory).(dao.Factory)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"expected Factory in context but got %T\", ctx.Value(internal.KeyFactory))\n\t}\n\ta.Init(factory, t.gvr)\n\n\tt.mx.RLock()\n\tctx = context.WithValue(ctx, internal.KeyLabels, t.labelSelector)\n\tt.mx.RUnlock()\n\n\tns := client.CleanseNamespace(t.data.GetNamespace())\n\tif client.IsClusterScoped(ns) {\n\t\tns = client.BlankNamespace\n\t}\n\n\treturn a.List(ctx, ns)\n}\n\nfunc (t *Table) reconcile(ctx context.Context) error {\n\tvar (\n\t\too  []runtime.Object\n\t\terr error\n\t)\n\tmeta := resourceMeta(t.gvr)\n\tif t.vs != nil {\n\t\tmeta.DAO.SetIncludeObject(true)\n\t}\n\tctx = context.WithValue(ctx, internal.KeyLabels, t.labelSelector)\n\tif t.instance == \"\" {\n\t\too, err = t.list(ctx, meta.DAO)\n\t} else {\n\t\to, e := t.Get(ctx, t.instance)\n\t\too, err = []runtime.Object{o}, e\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\tr := meta.Renderer\n\tr.SetViewSetting(t.vs)\n\n\treturn t.data.Render(ctx, meta.Renderer, oo)\n}\n\nfunc (t *Table) fireTableChanged(data *model1.TableData) {\n\tvar ll []TableListener\n\tt.mx.RLock()\n\tll = t.listeners\n\tt.mx.RUnlock()\n\n\tfor _, l := range ll {\n\t\tl.TableDataChanged(data)\n\t}\n}\n\nfunc (t *Table) fireNoData(data *model1.TableData) {\n\tvar ll []TableListener\n\tt.mx.RLock()\n\tll = t.listeners\n\tt.mx.RUnlock()\n\n\tfor _, l := range ll {\n\t\tl.TableNoData(data)\n\t}\n}\n\nfunc (t *Table) fireTableLoadFailed(err error) {\n\tvar ll []TableListener\n\tt.mx.RLock()\n\tll = t.listeners\n\tt.mx.RUnlock()\n\n\tfor _, l := range ll {\n\t\tl.TableLoadFailed(err)\n\t}\n}\n"
  },
  {
    "path": "internal/model/table_int_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/watch\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/client-go/informers\"\n)\n\nfunc TestTableReconcile(t *testing.T) {\n\tta := NewTable(client.PodGVR)\n\tta.SetNamespace(client.NamespaceAll)\n\n\tf := makeFactory()\n\tf.rows = []runtime.Object{load(t, \"p1\")}\n\tctx := context.WithValue(context.Background(), internal.KeyFactory, f)\n\tctx = context.WithValue(ctx, internal.KeyFields, \"\")\n\tctx = context.WithValue(ctx, internal.KeyWithMetrics, false)\n\terr := ta.reconcile(ctx)\n\trequire.NoError(t, err)\n\tdata := ta.Peek()\n\tassert.Equal(t, 26, data.HeaderCount())\n\tassert.Equal(t, 1, data.RowCount())\n\tassert.Equal(t, client.NamespaceAll, data.GetNamespace())\n}\n\nfunc TestTableList(t *testing.T) {\n\tta := NewTable(client.PodGVR)\n\tta.SetNamespace(\"blee\")\n\n\tacc := accessor{}\n\tctx := context.WithValue(context.Background(), internal.KeyFactory, makeFactory())\n\trows, err := ta.list(ctx, &acc)\n\trequire.NoError(t, err)\n\tassert.Len(t, rows, 1)\n}\n\nfunc TestTableGet(t *testing.T) {\n\tta := NewTable(client.PodGVR)\n\tta.SetNamespace(\"blee\")\n\n\tf := makeFactory()\n\tf.rows = []runtime.Object{load(t, \"p1\")}\n\tctx := context.WithValue(context.Background(), internal.KeyFactory, f)\n\tctx = context.WithValue(ctx, internal.KeyWithMetrics, false)\n\trow, err := ta.Get(ctx, \"fred\")\n\trequire.NoError(t, err)\n\tassert.NotNil(t, row)\n\tassert.Len(t, row.(*render.PodWithMetrics).Raw.Object, 5)\n}\n\nfunc TestTableMeta(t *testing.T) {\n\tuu := map[string]struct {\n\t\tgvr      *client.GVR\n\t\taccessor dao.Accessor\n\t\trenderer model1.Renderer\n\t}{\n\t\t\"generic\": {\n\t\t\tgvr:      client.CoGVR,\n\t\t\taccessor: &dao.Container{},\n\t\t\trenderer: &render.Container{},\n\t\t},\n\t\t\"node\": {\n\t\t\tgvr:      client.NodeGVR,\n\t\t\taccessor: &dao.Node{},\n\t\t\trenderer: &render.Node{},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tta := NewTable(u.gvr)\n\t\tm := resourceMeta(ta.gvr)\n\n\t\tassert.Equal(t, u.accessor, m.DAO)\n\t\tassert.Equal(t, u.renderer, m.Renderer)\n\t}\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc mustLoad(n string) *unstructured.Unstructured {\n\traw, err := os.ReadFile(fmt.Sprintf(\"testdata/%s.json\", n))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tvar o unstructured.Unstructured\n\tif err = json.Unmarshal(raw, &o); err != nil {\n\t\tpanic(err)\n\t}\n\treturn &o\n}\n\nfunc load(t *testing.T, n string) *unstructured.Unstructured {\n\traw, err := os.ReadFile(fmt.Sprintf(\"testdata/%s.json\", n))\n\trequire.NoError(t, err)\n\tvar o unstructured.Unstructured\n\terr = json.Unmarshal(raw, &o)\n\trequire.NoError(t, err)\n\treturn &o\n}\n\n// ----------------------------------------------------------------------------\n\nfunc makeFactory() testFactory {\n\treturn testFactory{}\n}\n\ntype testFactory struct {\n\trows []runtime.Object\n}\n\nvar _ dao.Factory = testFactory{}\n\nfunc (testFactory) Client() client.Connection {\n\treturn client.NewTestAPIClient()\n}\n\nfunc (f testFactory) Get(*client.GVR, string, bool, labels.Selector) (runtime.Object, error) {\n\tif len(f.rows) > 0 {\n\t\treturn f.rows[0], nil\n\t}\n\treturn nil, nil\n}\n\nfunc (f testFactory) List(*client.GVR, string, bool, labels.Selector) ([]runtime.Object, error) {\n\tif len(f.rows) > 0 {\n\t\treturn f.rows, nil\n\t}\n\treturn nil, nil\n}\n\nfunc (testFactory) ForResource(string, *client.GVR) (informers.GenericInformer, error) {\n\treturn nil, nil\n}\n\nfunc (testFactory) CanForResource(string, *client.GVR, []string) (informers.GenericInformer, error) {\n\treturn nil, nil\n}\nfunc (testFactory) WaitForCacheSync() {}\nfunc (testFactory) Forwarders() watch.Forwarders {\n\treturn nil\n}\nfunc (testFactory) DeleteForwarder(string) {}\n\n// ----------------------------------------------------------------------------\n\ntype accessor struct {\n\tgvr *client.GVR\n}\n\nvar _ dao.Accessor = (*accessor)(nil)\n\nfunc (*accessor) SetIncludeObject(bool) {}\n\nfunc (*accessor) List(context.Context, string) ([]runtime.Object, error) {\n\treturn []runtime.Object{&render.PodWithMetrics{Raw: mustLoad(\"p1\")}}, nil\n}\n\nfunc (*accessor) Get(context.Context, string) (runtime.Object, error) {\n\treturn &render.PodWithMetrics{Raw: mustLoad(\"p1\")}, nil\n}\n\nfunc (a *accessor) Init(_ dao.Factory, gvr *client.GVR) {\n\ta.gvr = gvr\n}\n\nfunc (a *accessor) GVR() string {\n\treturn a.gvr.String()\n}\n"
  },
  {
    "path": "internal/model/table_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/watch\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/client-go/informers\"\n)\n\nfunc TestTableRefresh(t *testing.T) {\n\tta := model.NewTable(client.PodGVR)\n\tta.SetNamespace(client.NamespaceAll)\n\n\tl := tableListener{}\n\tta.AddListener(&l)\n\tf := makeTableFactory()\n\tf.rows = []runtime.Object{mustLoad(\"p1\")}\n\tctx := context.WithValue(context.Background(), internal.KeyFactory, f)\n\tctx = context.WithValue(ctx, internal.KeyFields, \"\")\n\tctx = context.WithValue(ctx, internal.KeyWithMetrics, false)\n\trequire.NoError(t, ta.Refresh(ctx))\n\tdata := ta.Peek()\n\tassert.Equal(t, 26, data.HeaderCount())\n\tassert.Equal(t, 1, data.RowCount())\n\tassert.Equal(t, client.NamespaceAll, data.GetNamespace())\n\tassert.Equal(t, 1, l.count)\n\tassert.Equal(t, 0, l.errs)\n}\n\nfunc TestTableNS(t *testing.T) {\n\tta := model.NewTable(client.PodGVR)\n\tta.SetNamespace(\"blee\")\n\n\tassert.Equal(t, \"blee\", ta.GetNamespace())\n\tassert.False(t, ta.ClusterWide())\n\tassert.False(t, ta.InNamespace(\"zorg\"))\n}\n\nfunc TestTableAddListener(t *testing.T) {\n\tta := model.NewTable(client.PodGVR)\n\tta.SetNamespace(\"blee\")\n\n\tassert.True(t, ta.Empty())\n\tl := tableListener{}\n\tta.AddListener(&l)\n}\n\nfunc TestTableRmListener(*testing.T) {\n\tta := model.NewTable(client.PodGVR)\n\tta.SetNamespace(\"blee\")\n\n\tl := tableListener{}\n\tta.RemoveListener(&l)\n}\n\n// Helpers...\n\ntype tableListener struct {\n\tcount, errs int\n}\n\nfunc (*tableListener) TableNoData(*model1.TableData) {}\n\nfunc (l *tableListener) TableDataChanged(*model1.TableData) {\n\tl.count++\n}\n\nfunc (l *tableListener) TableLoadFailed(error) {\n\tl.errs++\n}\n\ntype tableFactory struct {\n\trows []runtime.Object\n}\n\nvar _ dao.Factory = tableFactory{}\n\nfunc (tableFactory) Client() client.Connection {\n\treturn client.NewTestAPIClient()\n}\n\nfunc (f tableFactory) Get(*client.GVR, string, bool, labels.Selector) (runtime.Object, error) {\n\tif len(f.rows) > 0 {\n\t\treturn f.rows[0], nil\n\t}\n\treturn nil, nil\n}\n\nfunc (f tableFactory) List(*client.GVR, string, bool, labels.Selector) ([]runtime.Object, error) {\n\tif len(f.rows) > 0 {\n\t\treturn f.rows, nil\n\t}\n\treturn nil, nil\n}\n\nfunc (tableFactory) ForResource(string, *client.GVR) (informers.GenericInformer, error) {\n\treturn nil, nil\n}\n\nfunc (tableFactory) CanForResource(string, *client.GVR, []string) (informers.GenericInformer, error) {\n\treturn nil, nil\n}\nfunc (tableFactory) WaitForCacheSync() {}\nfunc (tableFactory) Forwarders() watch.Forwarders {\n\treturn nil\n}\nfunc (tableFactory) DeleteForwarder(string) {}\n\nfunc makeTableFactory() tableFactory {\n\treturn tableFactory{}\n}\n\nfunc mustLoad(n string) *unstructured.Unstructured {\n\traw, err := os.ReadFile(fmt.Sprintf(\"testdata/%s.json\", n))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tvar o unstructured.Unstructured\n\tif err = json.Unmarshal(raw, &o); err != nil {\n\t\tpanic(err)\n\t}\n\treturn &o\n}\n"
  },
  {
    "path": "internal/model/testdata/p1.json",
    "content": "{\n  \"apiVersion\": \"v1\",\n  \"kind\": \"Pod\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/restartedAt\": \"2019-12-31T12:26:47-07:00\"\n    },\n    \"creationTimestamp\": \"2019-12-31T19:27:22Z\",\n    \"generateName\": \"nginx-7fb78fb6d8-\",\n    \"labels\": {\n      \"app\": \"nginx\",\n      \"pod-template-hash\": \"7fb78fb6d8\"\n    },\n    \"name\": \"nginx-7fb78fb6d8-2w75j\",\n    \"namespace\": \"default\",\n    \"ownerReferences\": [\n      {\n        \"apiVersion\": \"apps/v1\",\n        \"blockOwnerDeletion\": true,\n        \"controller\": true,\n        \"kind\": \"ReplicaSet\",\n        \"name\": \"nginx-7fb78fb6d8\",\n        \"uid\": \"7ccd0600-2c03-11ea-883f-42010a800044\"\n      }\n    ],\n    \"resourceVersion\": \"87290191\",\n    \"selfLink\": \"/api/v1/namespaces/default/pods/nginx-7fb78fb6d8-2w75j\",\n    \"uid\": \"91bb1cf2-2c03-11ea-883f-42010a800044\"\n  },\n  \"spec\": {\n    \"containers\": [\n      {\n        \"image\": \"k8s.gcr.io/nginx-slim:0.8\",\n        \"imagePullPolicy\": \"IfNotPresent\",\n        \"name\": \"nginx\",\n        \"ports\": [\n          {\n            \"containerPort\": 80,\n            \"protocol\": \"TCP\"\n          }\n        ],\n        \"resources\": {\n          \"limits\": {\n            \"cpu\": \"200m\",\n            \"memory\": \"20Mi\"\n          },\n          \"requests\": {\n            \"cpu\": \"200m\",\n            \"memory\": \"20Mi\"\n          }\n        },\n        \"terminationMessagePath\": \"/dev/termination-log\",\n        \"terminationMessagePolicy\": \"File\",\n        \"volumeMounts\": [\n          {\n            \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\",\n            \"name\": \"default-token-dsl46\",\n            \"readOnly\": true\n          }\n        ]\n      }\n    ],\n    \"dnsPolicy\": \"ClusterFirst\",\n    \"enableServiceLinks\": true,\n    \"nodeName\": \"gke-k9s-default-pool-0fa2fb89-lbtf\",\n    \"priority\": 0,\n    \"restartPolicy\": \"Always\",\n    \"schedulerName\": \"default-scheduler\",\n    \"securityContext\": {},\n    \"serviceAccount\": \"default\",\n    \"serviceAccountName\": \"default\",\n    \"terminationGracePeriodSeconds\": 30,\n    \"tolerations\": [\n      {\n        \"effect\": \"NoExecute\",\n        \"key\": \"node.kubernetes.io/not-ready\",\n        \"operator\": \"Exists\",\n        \"tolerationSeconds\": 300\n      },\n      {\n        \"effect\": \"NoExecute\",\n        \"key\": \"node.kubernetes.io/unreachable\",\n        \"operator\": \"Exists\",\n        \"tolerationSeconds\": 300\n      }\n    ],\n    \"volumes\": [\n      {\n        \"name\": \"default-token-dsl46\",\n        \"secret\": {\n          \"defaultMode\": 420,\n          \"secretName\": \"default-token-dsl46\"\n        }\n      }\n    ]\n  },\n  \"status\": {\n    \"conditions\": [\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-12-31T19:27:23Z\",\n        \"status\": \"True\",\n        \"type\": \"Initialized\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-12-31T19:27:25Z\",\n        \"status\": \"True\",\n        \"type\": \"Ready\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-12-31T19:27:25Z\",\n        \"status\": \"True\",\n        \"type\": \"ContainersReady\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-12-31T19:27:22Z\",\n        \"status\": \"True\",\n        \"type\": \"PodScheduled\"\n      }\n    ],\n    \"containerStatuses\": [\n      {\n        \"containerID\": \"docker://90e0abf7a779dd76d36038883312baed57a8351428a1d6340df3cff698f51809\",\n        \"image\": \"k8s.gcr.io/nginx-slim:0.8\",\n        \"imageID\": \"docker-pullable://k8s.gcr.io/nginx-slim@sha256:8b4501fe0fe221df663c22e16539f399e89594552f400408303c42f3dd8d0e52\",\n        \"lastState\": {},\n        \"name\": \"nginx\",\n        \"ready\": true,\n        \"restartCount\": 0,\n        \"state\": {\n          \"running\": {\n            \"startedAt\": \"2019-12-31T19:27:24Z\"\n          }\n        }\n      }\n    ],\n    \"hostIP\": \"10.128.0.15\",\n    \"phase\": \"Running\",\n    \"podIP\": \"10.44.0.229\",\n    \"qosClass\": \"Guaranteed\",\n    \"startTime\": \"2019-12-31T19:27:23Z\"\n  }\n}"
  },
  {
    "path": "internal/model/text.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/sahilm/fuzzy\"\n)\n\n// Filterable represents an entity that can be filtered.\ntype Filterable interface {\n\tFilter(string)\n\tClearFilter()\n}\n\n// Textable represents a text resource.\ntype Textable interface {\n\tPeek() []string\n\tSetText(string)\n\tAddListener(TextListener)\n\tRemoveListener(TextListener)\n}\n\n// TextListener represents a text model listener.\ntype TextListener interface {\n\t// TextChanged notifies the model changed.\n\tTextChanged([]string)\n\n\t// TextFiltered notifies when the filter changed.\n\tTextFiltered([]string, fuzzy.Matches)\n}\n\n// Text represents a text model.\ntype Text struct {\n\tlines     []string\n\tlisteners []TextListener\n\tquery     string\n}\n\n// NewText returns a new model.\nfunc NewText() *Text {\n\treturn &Text{}\n}\n\n// Peek returns the current model state.\nfunc (t *Text) Peek() []string {\n\treturn t.lines\n}\n\n// ClearFilter clear out filter.\nfunc (t *Text) ClearFilter() {\n\tt.query = \"\"\n\tt.filterChanged(t.lines)\n}\n\n// Filter filters out the text.\nfunc (t *Text) Filter(q string) {\n\tt.query = q\n\tt.filterChanged(t.lines)\n}\n\n// SetText sets the current model content.\nfunc (t *Text) SetText(buff string) {\n\tt.lines = strings.Split(buff, \"\\n\")\n\tt.fireTextChanged(t.lines)\n}\n\n// AddListener adds a new model listener.\nfunc (t *Text) AddListener(listener TextListener) {\n\tt.listeners = append(t.listeners, listener)\n}\n\n// RemoveListener delete a listener from the list.\nfunc (t *Text) RemoveListener(listener TextListener) {\n\tvictim := -1\n\tfor i, lis := range t.listeners {\n\t\tif lis == listener {\n\t\t\tvictim = i\n\t\t\tbreak\n\t\t}\n\t}\n\tif victim >= 0 {\n\t\tt.listeners = append(t.listeners[:victim], t.listeners[victim+1:]...)\n\t}\n}\n\nfunc (t *Text) filterChanged(lines []string) {\n\tt.fireTextFiltered(lines, t.filter(t.query, lines))\n}\n\nfunc (t *Text) fireTextChanged(lines []string) {\n\tfor _, lis := range t.listeners {\n\t\tlis.TextChanged(lines)\n\t}\n}\n\nfunc (t *Text) fireTextFiltered(lines []string, matches fuzzy.Matches) {\n\tfor _, lis := range t.listeners {\n\t\tlis.TextFiltered(lines, matches)\n\t}\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc (t *Text) filter(q string, lines []string) fuzzy.Matches {\n\tif q == \"\" {\n\t\treturn nil\n\t}\n\tif f, ok := internal.IsFuzzySelector(q); ok {\n\t\treturn t.fuzzyFilter(strings.TrimSpace(f), lines)\n\t}\n\treturn rxFilter(q, lines)\n}\n\nfunc (*Text) fuzzyFilter(q string, lines []string) fuzzy.Matches {\n\treturn fuzzy.Find(q, lines)\n}\n"
  },
  {
    "path": "internal/model/text_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/sahilm/fuzzy\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNewText(t *testing.T) {\n\tm := model.NewText()\n\n\tlis := textLis{}\n\tm.AddListener(&lis)\n\n\tm.SetText(\"Hello World\\nBumbleBeeTuna\")\n\n\tassert.Equal(t, 1, lis.changed)\n\tassert.Equal(t, 2, lis.lines)\n\tassert.Equal(t, 0, lis.filtered)\n\tassert.Equal(t, 0, lis.matches)\n}\n\nfunc TestTextFilterRXMatch(t *testing.T) {\n\tm := model.NewText()\n\n\tlis := textLis{}\n\tm.AddListener(&lis)\n\n\tm.SetText(\"Hello World\\nBumbleBeeTuna\")\n\tm.Filter(\"world\")\n\n\tassert.Equal(t, 1, lis.changed)\n\tassert.Equal(t, 2, lis.lines)\n\tassert.Equal(t, 1, lis.filtered)\n\tassert.Equal(t, 1, lis.matches)\n\tassert.Equal(t, 6, lis.index)\n}\n\nfunc TestTextFilterFuzzyMatch(t *testing.T) {\n\tm := model.NewText()\n\n\tlis := textLis{}\n\tm.AddListener(&lis)\n\n\tm.SetText(\"Hello World\\nBumbleBeeTuna\")\n\tm.Filter(\"-f world\")\n\n\tassert.Equal(t, 1, lis.changed)\n\tassert.Equal(t, 2, lis.lines)\n\tassert.Equal(t, 1, lis.filtered)\n\tassert.Equal(t, 1, lis.matches)\n\tassert.Equal(t, 6, lis.index)\n}\n\nfunc TestTextFilterNoMatch(t *testing.T) {\n\tm := model.NewText()\n\n\tlis := textLis{}\n\tm.AddListener(&lis)\n\n\tm.SetText(\"Hello World\\nBumbleBeeTuna\")\n\tm.Filter(\"blee\")\n\n\tassert.Equal(t, 1, lis.changed)\n\tassert.Equal(t, 2, lis.lines)\n\tassert.Equal(t, 1, lis.filtered)\n\tassert.Equal(t, 0, lis.matches)\n\tassert.Equal(t, 0, lis.index)\n}\n\n// Helpers...\n\ntype textLis struct {\n\tchanged, filtered, matches, lines, index int\n}\n\nfunc (l *textLis) TextChanged(ll []string) {\n\tl.lines = len(ll)\n\tl.changed++\n}\n\nfunc (l *textLis) TextFiltered(_ []string, mm fuzzy.Matches) {\n\tl.matches = len(mm)\n\tl.filtered++\n\tif len(mm) > 0 && len(mm[0].MatchedIndexes) > 0 {\n\t\tl.index = mm[0].MatchedIndexes[0]\n\t}\n}\n"
  },
  {
    "path": "internal/model/tree.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/xray\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nconst initTreeRefreshRate = 500 * time.Millisecond\n\n// TreeListener represents a tree model listener.\ntype TreeListener interface {\n\t// TreeChanged notifies the model data changed.\n\tTreeChanged(*xray.TreeNode)\n\n\t// TreeLoadFailed notifies the load failed.\n\tTreeLoadFailed(error)\n}\n\n// Tree represents a tree model.\ntype Tree struct {\n\tgvr         *client.GVR\n\tnamespace   string\n\troot        *xray.TreeNode\n\tlisteners   []TreeListener\n\tinUpdate    int32\n\trefreshRate time.Duration\n\tquery       string\n}\n\n// NewTree returns a new model.\nfunc NewTree(gvr *client.GVR) *Tree {\n\treturn &Tree{\n\t\tgvr:         gvr,\n\t\trefreshRate: 2 * time.Second,\n\t}\n}\n\n// ClearFilter clears out active filter.\nfunc (t *Tree) ClearFilter() {\n\tt.query = \"\"\n}\n\n// SetFilter sets the current filter.\nfunc (t *Tree) SetFilter(q string) {\n\tt.query = q\n}\n\n// AddListener adds a listener.\nfunc (t *Tree) AddListener(l TreeListener) {\n\tt.listeners = append(t.listeners, l)\n}\n\n// RemoveListener delete a listener.\nfunc (t *Tree) RemoveListener(l TreeListener) {\n\tvictim := -1\n\tfor i, lis := range t.listeners {\n\t\tif lis == l {\n\t\t\tvictim = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif victim >= 0 {\n\t\tt.listeners = append(t.listeners[:victim], t.listeners[victim+1:]...)\n\t}\n}\n\n// Watch initiates model updates.\nfunc (t *Tree) Watch(ctx context.Context) {\n\tt.Refresh(ctx)\n\tgo t.updater(ctx)\n}\n\n// Refresh update the model now.\nfunc (t *Tree) Refresh(ctx context.Context) {\n\tt.refresh(ctx)\n}\n\n// GetNamespace returns the model namespace.\nfunc (t *Tree) GetNamespace() string {\n\treturn t.namespace\n}\n\n// SetNamespace sets up model namespace.\nfunc (t *Tree) SetNamespace(ns string) {\n\tt.namespace = ns\n\tif t.root == nil {\n\t\treturn\n\t}\n\tt.root.Clear()\n}\n\n// SetRefreshRate sets model refresh duration.\nfunc (t *Tree) SetRefreshRate(d time.Duration) {\n\tt.refreshRate = d\n}\n\n// ClusterWide checks if resource is scope for all namespaces.\nfunc (t *Tree) ClusterWide() bool {\n\treturn client.IsClusterWide(t.namespace)\n}\n\n// InNamespace checks if current namespace matches desired namespace.\nfunc (t *Tree) InNamespace(ns string) bool {\n\treturn t.namespace == ns\n}\n\n// Empty return true if no model data.\nfunc (t *Tree) Empty() bool {\n\treturn t.root.IsLeaf()\n}\n\n// Peek returns model data.\nfunc (t *Tree) Peek() *xray.TreeNode {\n\treturn t.root\n}\n\n// Describe describes a given resource.\nfunc (t *Tree) Describe(ctx context.Context, gvr *client.GVR, path string) (string, error) {\n\tmeta, err := t.getMeta(ctx, gvr)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdesc, ok := meta.DAO.(dao.Describer)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"no describer for %q\", meta.DAO.GVR())\n\t}\n\n\treturn desc.Describe(path)\n}\n\n// ToYAML returns a resource yaml.\nfunc (t *Tree) ToYAML(ctx context.Context, gvr *client.GVR, path string) (string, error) {\n\tmeta, err := t.getMeta(ctx, gvr)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdesc, ok := meta.DAO.(dao.Describer)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"no describer for %q\", meta.DAO.GVR())\n\t}\n\n\treturn desc.ToYAML(path, false)\n}\n\nfunc (t *Tree) updater(ctx context.Context) {\n\tdefer slog.Debug(\"Tree-model canceled\", slogs.GVR, t.gvr)\n\n\trate := initTreeRefreshRate\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tt.root = nil\n\t\t\treturn\n\t\tcase <-time.After(rate):\n\t\t\trate = t.refreshRate\n\t\t\tt.refresh(ctx)\n\t\t}\n\t}\n}\n\nfunc (t *Tree) refresh(ctx context.Context) {\n\tif !atomic.CompareAndSwapInt32(&t.inUpdate, 0, 1) {\n\t\tslog.Debug(\"Dropping update...\")\n\t\treturn\n\t}\n\tdefer atomic.StoreInt32(&t.inUpdate, 0)\n\n\tif err := t.reconcile(ctx); err != nil {\n\t\tslog.Error(\"Reconcile failed\", slogs.Error, err)\n\t\tt.fireTreeLoadFailed(err)\n\t\treturn\n\t}\n}\n\nfunc (t *Tree) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, error) {\n\tfactory, ok := ctx.Value(internal.KeyFactory).(dao.Factory)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"expected Factory in context but got %T\", ctx.Value(internal.KeyFactory))\n\t}\n\ta.Init(factory, t.gvr)\n\n\treturn a.List(ctx, client.CleanseNamespace(t.namespace))\n}\n\nfunc (t *Tree) reconcile(ctx context.Context) error {\n\tmeta := t.resourceMeta()\n\too, err := t.list(ctx, meta.DAO)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tns := client.CleanseNamespace(t.namespace)\n\troot := xray.NewTreeNode(t.gvr, t.gvr.R())\n\tctx = context.WithValue(ctx, xray.KeyParent, root)\n\tif _, ok := meta.TreeRenderer.(*xray.Generic); ok {\n\t\ttable, ok := oo[0].(*metav1.Table)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"expecting a Table but got %T\", oo[0])\n\t\t}\n\t\tif err := genericTreeHydrate(ctx, ns, table, meta.TreeRenderer); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else if err := treeHydrate(ctx, ns, oo, meta.TreeRenderer); err != nil {\n\t\treturn err\n\t}\n\n\troot.Sort()\n\tif t.query != \"\" {\n\t\tt.root = root.Filter(t.query, rxMatch)\n\t}\n\tif t.root == nil || t.root.Diff(root) {\n\t\tt.root = root\n\t\tt.fireTreeChanged(t.root)\n\t}\n\n\treturn nil\n}\n\nfunc (t *Tree) resourceMeta() ResourceMeta {\n\tmeta, ok := Registry[t.gvr]\n\tif !ok {\n\t\tmeta = ResourceMeta{\n\t\t\tDAO:      &dao.Table{},\n\t\t\tRenderer: &render.Table{},\n\t\t}\n\t}\n\tif meta.DAO == nil {\n\t\tmeta.DAO = &dao.Resource{}\n\t}\n\n\treturn meta\n}\n\nfunc (t *Tree) fireTreeChanged(root *xray.TreeNode) {\n\tfor _, l := range t.listeners {\n\t\tl.TreeChanged(root)\n\t}\n}\n\nfunc (t *Tree) fireTreeLoadFailed(err error) {\n\tfor _, l := range t.listeners {\n\t\tl.TreeLoadFailed(err)\n\t}\n}\n\nfunc (t *Tree) getMeta(ctx context.Context, gvr *client.GVR) (ResourceMeta, error) {\n\tmeta := t.resourceMeta()\n\tfactory, ok := ctx.Value(internal.KeyFactory).(dao.Factory)\n\tif !ok {\n\t\treturn ResourceMeta{}, fmt.Errorf(\"expected Factory in context but got %T\", ctx.Value(internal.KeyFactory))\n\t}\n\tmeta.DAO.Init(factory, gvr)\n\n\treturn meta, nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc rxMatch(q, path string) bool {\n\trx := regexp.MustCompile(`(?i)` + q)\n\n\ttokens := strings.Split(path, \"::\")\n\tfor _, t := range tokens {\n\t\tif rx.MatchString(t) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc treeHydrate(ctx context.Context, ns string, oo []runtime.Object, re TreeRenderer) error {\n\tif re == nil {\n\t\treturn fmt.Errorf(\"no tree renderer defined for this resource\")\n\t}\n\tpool := internal.NewWorkerPool(ctx, internal.DefaultPoolSize)\n\tfor _, o := range oo {\n\t\tpool.Add(func(_ context.Context) error {\n\t\t\treturn re.Render(ctx, ns, o)\n\t\t})\n\t}\n\terrs := pool.Drain()\n\tif len(errs) > 0 {\n\t\treturn errs[0]\n\t}\n\n\treturn nil\n}\n\nfunc genericTreeHydrate(ctx context.Context, ns string, table *metav1.Table, re TreeRenderer) error {\n\ttre, ok := re.(*xray.Generic)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting xray.Generic renderer but got %T\", re)\n\t}\n\n\ttre.SetTable(ns, table)\n\t// BOZO!! Need table row sorter!!\n\tfor _, row := range table.Rows {\n\t\tif err := tre.Render(ctx, ns, row); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/model/types.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n\t\"github.com/derailed/tview\"\n\t\"github.com/sahilm/fuzzy\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nconst (\n\tmaxReaderRetryInterval   = 2 * time.Minute\n\tdefaultReaderRefreshRate = 5 * time.Second\n)\n\n// ResourceViewerListener listens to viewing resource events.\ntype ResourceViewerListener interface {\n\tResourceChanged(lines []string, matches fuzzy.Matches)\n\tResourceFailed(error)\n}\n\n// ViewerToggleOpts represents a collection of viewing options.\ntype ViewerToggleOpts map[string]bool\n\n// ResourceViewer represents a viewed resource.\ntype ResourceViewer interface {\n\tGetPath() string\n\tFilter(string)\n\tGVR() *client.GVR\n\tClearFilter()\n\tPeek() []string\n\tSetOptions(context.Context, ViewerToggleOpts)\n\tWatch(context.Context) error\n\tRefresh(context.Context) error\n\tAddListener(ResourceViewerListener)\n\tRemoveListener(ResourceViewerListener)\n}\n\n// EncDecResourceViewer interface extends the ResourceViewer interface and\n// adds a `Toggle` that allows the user to switch between encoded or decoded\n// state of the view.\ntype EncDecResourceViewer interface {\n\tResourceViewer\n\tToggle()\n}\n\n// Igniter represents a runnable view.\ntype Igniter interface {\n\t// Start starts a component.\n\tInit(ctx context.Context) error\n\n\t// Start starts a component.\n\tStart()\n\n\t// Stop terminates a component.\n\tStop()\n}\n\n// Hinter represent a menu mnemonic provider.\ntype Hinter interface {\n\t// Hints returns a collection of menu hints.\n\tHints() MenuHints\n\n\t// ExtraHints returns additional hints.\n\tExtraHints() map[string]string\n}\n\n// Primitive represents a UI primitive.\ntype Primitive interface {\n\ttview.Primitive\n\n\t// Name returns the view name.\n\tName() string\n}\n\n// Commander tracks prompt status.\ntype Commander interface {\n\t// InCmdMode checks if prompt is active.\n\tInCmdMode() bool\n}\n\n// Component represents a ui component.\ntype Component interface {\n\tPrimitive\n\tIgniter\n\tHinter\n\tCommander\n\tFilterer\n\tViewer\n}\n\n// Viewer represents a resource viewer.\ntype Viewer interface {\n\t// SetCommand sets the current command.\n\tSetCommand(*cmd.Interpreter)\n}\n\n// Filterer represents a filterable component.\ntype Filterer interface {\n\t// SetFilter sets the filter text.\n\tSetFilter(string, bool)\n\n\t// SetLabelSelector sets the label selector.\n\tSetLabelSelector(labels.Selector, bool)\n}\n\n// Cruder performs crud operations.\ntype Cruder interface {\n\t// List returns a collection of resources.\n\tList(ctx context.Context, ns string) ([]runtime.Object, error)\n\n\t// Get returns a resource instance.\n\tGet(ctx context.Context, path string) (runtime.Object, error)\n}\n\n// Lister represents a resource lister.\ntype Lister interface {\n\tCruder\n\n\t// Init initializes a resource.\n\tInit(ns, gvr string, f dao.Factory)\n}\n\n// Describer represents a resource describer.\ntype Describer interface {\n\t// ToYAML return resource yaml.\n\tToYAML(ctx context.Context, path string) (string, error)\n\n\t// Describe returns a resource description.\n\tDescribe(client client.Connection, gvr, path string) (string, error)\n}\n\n// TreeRenderer represents an xray node.\ntype TreeRenderer interface {\n\tRender(ctx context.Context, ns string, o any) error\n}\n\n// ResourceMeta represents model info about a resource.\ntype ResourceMeta struct {\n\tDAO          dao.Accessor\n\tRenderer     model1.Renderer\n\tTreeRenderer TreeRenderer\n}\n"
  },
  {
    "path": "internal/model/values.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\tbackoff \"github.com/cenkalti/backoff/v4\"\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/sahilm/fuzzy\"\n)\n\n// Values tracks Helm values representations.\ntype Values struct {\n\tfactory   dao.Factory\n\tgvr       *client.GVR\n\tinUpdate  int32\n\tpath      string\n\tquery     string\n\tlines     []string\n\tallValues bool\n\tlisteners []ResourceViewerListener\n\toptions   ViewerToggleOpts\n}\n\n// NewValues return a new Helm values resource model.\nfunc NewValues(gvr *client.GVR, path string) *Values {\n\treturn &Values{\n\t\tgvr:       gvr,\n\t\tpath:      path,\n\t\tallValues: false,\n\t}\n}\n\n// Init initializes the model.\nfunc (v *Values) Init(f dao.Factory) error {\n\tv.factory = f\n\n\tvar err error\n\tv.lines, err = v.getValues()\n\n\treturn err\n}\n\nfunc (v *Values) getValues() ([]string, error) {\n\taccessor, err := dao.AccessorFor(v.factory, v.gvr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvaluer, ok := accessor.(dao.Valuer)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"resource %s is not Valuer\", v.gvr)\n\t}\n\n\tvalues, err := valuer.GetValues(v.path, v.allValues)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn strings.Split(string(values), \"\\n\"), nil\n}\n\n// GVR returns the resource gvr.\nfunc (v *Values) GVR() *client.GVR {\n\treturn v.gvr\n}\n\n// ToggleValues toggles between user supplied values and computed values.\nfunc (v *Values) ToggleValues() error {\n\tv.allValues = !v.allValues\n\n\tlines, err := v.getValues()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tv.lines = lines\n\treturn nil\n}\n\n// GetPath returns the active resource path.\nfunc (v *Values) GetPath() string {\n\treturn v.path\n}\n\n// SetOptions toggle model options.\nfunc (v *Values) SetOptions(ctx context.Context, opts ViewerToggleOpts) {\n\tv.options = opts\n\tif err := v.refresh(ctx); err != nil {\n\t\tv.fireResourceFailed(err)\n\t}\n}\n\n// Filter filters the model.\nfunc (v *Values) Filter(q string) {\n\tv.query = q\n\tv.filterChanged(v.lines)\n}\n\nfunc (v *Values) filterChanged(lines []string) {\n\tv.fireResourceChanged(lines, v.filter(v.query, lines))\n}\n\nfunc (v *Values) filter(q string, lines []string) fuzzy.Matches {\n\tif q == \"\" {\n\t\treturn nil\n\t}\n\tif f, ok := internal.IsFuzzySelector(q); ok {\n\t\treturn v.fuzzyFilter(strings.TrimSpace(f), lines)\n\t}\n\treturn rxFilter(q, lines)\n}\n\nfunc (*Values) fuzzyFilter(q string, lines []string) fuzzy.Matches {\n\treturn fuzzy.Find(q, lines)\n}\n\nfunc (v *Values) fireResourceChanged(lines []string, matches fuzzy.Matches) {\n\tfor _, l := range v.listeners {\n\t\tl.ResourceChanged(lines, matches)\n\t}\n}\n\nfunc (v *Values) fireResourceFailed(err error) {\n\tfor _, l := range v.listeners {\n\t\tl.ResourceFailed(err)\n\t}\n}\n\n// ClearFilter clear out the filter.\nfunc (v *Values) ClearFilter() {\n\tv.query = \"\"\n}\n\n// Peek returns the current model data.\nfunc (v *Values) Peek() []string {\n\treturn v.lines\n}\n\n// Refresh updates model data.\nfunc (v *Values) Refresh(ctx context.Context) error {\n\treturn v.refresh(ctx)\n}\n\n// Watch watches for Values changes.\nfunc (v *Values) Watch(ctx context.Context) error {\n\tif err := v.refresh(ctx); err != nil {\n\t\treturn err\n\t}\n\tgo v.updater(ctx)\n\n\treturn nil\n}\n\nfunc (v *Values) updater(ctx context.Context) {\n\tdefer slog.Debug(\"YAML canceled\", slogs.GVR, v.gvr)\n\n\tbackOff := NewExpBackOff(ctx, defaultReaderRefreshRate, maxReaderRetryInterval)\n\tdelay := defaultReaderRefreshRate\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-time.After(delay):\n\t\t\tif err := v.refresh(ctx); err != nil {\n\t\t\t\tv.fireResourceFailed(err)\n\t\t\t\tif delay = backOff.NextBackOff(); delay == backoff.Stop {\n\t\t\t\t\tslog.Error(\"Giving up retrieving chart values\", slogs.Error, err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tbackOff.Reset()\n\t\t\t\tdelay = defaultReaderRefreshRate\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (v *Values) refresh(context.Context) error {\n\tif !atomic.CompareAndSwapInt32(&v.inUpdate, 0, 1) {\n\t\tslog.Debug(\"Dropping update...\")\n\t\treturn fmt.Errorf(\"reconcile in progress. Dropping update\")\n\t}\n\tdefer atomic.StoreInt32(&v.inUpdate, 0)\n\n\tv.reconcile()\n\n\treturn nil\n}\n\nfunc (v *Values) reconcile() {\n\tv.fireResourceChanged(v.lines, v.filter(v.query, v.lines))\n}\n\n// AddListener adds a new model listener.\nfunc (v *Values) AddListener(l ResourceViewerListener) {\n\tv.listeners = append(v.listeners, l)\n}\n\n// RemoveListener delete a listener from the list.\nfunc (v *Values) RemoveListener(l ResourceViewerListener) {\n\tvictim := -1\n\tfor i, lis := range v.listeners {\n\t\tif lis == l {\n\t\t\tvictim = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif victim >= 0 {\n\t\tv.listeners = append(v.listeners[:victim], v.listeners[victim+1:]...)\n\t}\n}\n"
  },
  {
    "path": "internal/model/yaml.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"reflect\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\tbackoff \"github.com/cenkalti/backoff/v4\"\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/sahilm/fuzzy\"\n)\n\n// ManagedFieldsOpts tracks managed fields.\nconst ManagedFieldsOpts = \"ManagedFields\"\n\n// YAML tracks yaml resource representations.\ntype YAML struct {\n\tgvr       *client.GVR\n\tinUpdate  int32\n\tpath      string\n\tquery     string\n\tlines     []string\n\tlisteners []ResourceViewerListener\n\toptions   ViewerToggleOpts\n\tdecode    bool\n}\n\n// NewYAML return a new yaml resource model.\nfunc NewYAML(gvr *client.GVR, path string) *YAML {\n\treturn &YAML{\n\t\tgvr:  gvr,\n\t\tpath: path,\n\t}\n}\n\n// GVR returns the resource gvr.\nfunc (y *YAML) GVR() *client.GVR {\n\treturn y.gvr\n}\n\n// GetPath returns the active resource path.\nfunc (y *YAML) GetPath() string {\n\treturn y.path\n}\n\n// SetOptions toggle model options.\nfunc (y *YAML) SetOptions(ctx context.Context, opts ViewerToggleOpts) {\n\ty.options = opts\n\tif err := y.refresh(ctx); err != nil {\n\t\ty.fireResourceFailed(err)\n\t}\n}\n\n// Filter filters the model.\nfunc (y *YAML) Filter(q string) {\n\ty.query = q\n\ty.filterChanged(y.lines)\n}\n\nfunc (y *YAML) filterChanged(lines []string) {\n\ty.fireResourceChanged(lines, y.filter(y.query, lines))\n}\n\nfunc (y *YAML) filter(q string, lines []string) fuzzy.Matches {\n\tif q == \"\" {\n\t\treturn nil\n\t}\n\tif f, ok := internal.IsFuzzySelector(q); ok {\n\t\treturn y.fuzzyFilter(strings.TrimSpace(f), lines)\n\t}\n\treturn rxFilter(q, lines)\n}\n\nfunc (*YAML) fuzzyFilter(q string, lines []string) fuzzy.Matches {\n\treturn fuzzy.Find(q, lines)\n}\n\nfunc (y *YAML) fireResourceChanged(lines []string, matches fuzzy.Matches) {\n\tfor _, l := range y.listeners {\n\t\tl.ResourceChanged(lines, matches)\n\t}\n}\n\nfunc (y *YAML) fireResourceFailed(err error) {\n\tfor _, l := range y.listeners {\n\t\tl.ResourceFailed(err)\n\t}\n}\n\n// ClearFilter clear out the filter.\nfunc (y *YAML) ClearFilter() {\n\ty.query = \"\"\n}\n\n// Peek returns the current model data.\nfunc (y *YAML) Peek() []string {\n\treturn y.lines\n}\n\n// Refresh updates model data.\nfunc (y *YAML) Refresh(ctx context.Context) error {\n\treturn y.refresh(ctx)\n}\n\n// Watch watches for YAML changes.\nfunc (y *YAML) Watch(ctx context.Context) error {\n\tif err := y.refresh(ctx); err != nil {\n\t\treturn err\n\t}\n\tgo y.updater(ctx)\n\n\treturn nil\n}\n\nfunc (y *YAML) updater(ctx context.Context) {\n\tdefer slog.Debug(\"YAML canceled\", slogs.GVR, y.gvr)\n\n\tbackOff := NewExpBackOff(ctx, defaultReaderRefreshRate, maxReaderRetryInterval)\n\tdelay := defaultReaderRefreshRate\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-time.After(delay):\n\t\t\tif err := y.refresh(ctx); err != nil {\n\t\t\t\ty.fireResourceFailed(err)\n\t\t\t\tif delay = backOff.NextBackOff(); delay == backoff.Stop {\n\t\t\t\t\tslog.Error(\"YAML gave up!\", slogs.Error, err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tbackOff.Reset()\n\t\t\t\tdelay = defaultReaderRefreshRate\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (y *YAML) refresh(ctx context.Context) error {\n\tif !atomic.CompareAndSwapInt32(&y.inUpdate, 0, 1) {\n\t\tslog.Debug(\"Dropping update...\", slogs.GVR, y.gvr)\n\t\treturn nil\n\t}\n\tdefer atomic.StoreInt32(&y.inUpdate, 0)\n\n\tif err := y.reconcile(ctx); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (y *YAML) reconcile(ctx context.Context) error {\n\ts, err := y.ToYAML(ctx, y.gvr, y.path, y.options[ManagedFieldsOpts])\n\tif err != nil {\n\t\treturn err\n\t}\n\tlines := strings.Split(s, \"\\n\")\n\tif reflect.DeepEqual(lines, y.lines) {\n\t\treturn nil\n\t}\n\ty.lines = lines\n\ty.fireResourceChanged(y.lines, y.filter(y.query, y.lines))\n\n\treturn nil\n}\n\n// AddListener adds a new model listener.\nfunc (y *YAML) AddListener(l ResourceViewerListener) {\n\ty.listeners = append(y.listeners, l)\n}\n\n// RemoveListener delete a listener from the list.\nfunc (y *YAML) RemoveListener(l ResourceViewerListener) {\n\tvictim := -1\n\tfor i, lis := range y.listeners {\n\t\tif lis == l {\n\t\t\tvictim = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif victim >= 0 {\n\t\ty.listeners = append(y.listeners[:victim], y.listeners[victim+1:]...)\n\t}\n}\n\n// ToYAML returns a resource yaml.\nfunc (y *YAML) ToYAML(ctx context.Context, gvr *client.GVR, path string, showManaged bool) (string, error) {\n\tmeta, err := getMeta(ctx, gvr)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdesc, ok := meta.DAO.(dao.Describer)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"no describer for %q\", meta.DAO.GVR())\n\t}\n\tif desc, ok := meta.DAO.(*dao.Secret); ok {\n\t\tdesc.SetDecodeData(y.decode)\n\t}\n\n\treturn desc.ToYAML(path, showManaged)\n}\n\n// Toggle toggles the decode flag.\nfunc (y *YAML) Toggle() {\n\ty.decode = !y.decode\n}\n"
  },
  {
    "path": "internal/model1/color.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model1\n\nimport \"github.com/derailed/tcell/v2\"\n\nvar (\n\t// ModColor row modified color.\n\tModColor tcell.Color\n\n\t// AddColor row added color.\n\tAddColor tcell.Color\n\n\t// PendingColor row added color.\n\tPendingColor tcell.Color\n\n\t// ErrColor row err color.\n\tErrColor tcell.Color\n\n\t// StdColor row default color.\n\tStdColor tcell.Color\n\n\t// HighlightColor row highlight color.\n\tHighlightColor tcell.Color\n\n\t// KillColor row deleted color.\n\tKillColor tcell.Color\n\n\t// CompletedColor row completed color.\n\tCompletedColor tcell.Color\n)\n\n// DefaultColorer set the default table row colors.\nfunc DefaultColorer(ns string, h Header, re *RowEvent) tcell.Color {\n\tif !IsValid(ns, h, re.Row) {\n\t\treturn ErrColor\n\t}\n\n\t//nolint:exhaustive\n\tswitch re.Kind {\n\tcase EventAdd:\n\t\treturn AddColor\n\tcase EventUpdate:\n\t\treturn ModColor\n\tcase EventDelete:\n\t\treturn KillColor\n\tdefault:\n\t\treturn StdColor\n\t}\n}\n"
  },
  {
    "path": "internal/model1/color_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model1_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestDefaultColorer(t *testing.T) {\n\tuu := map[string]struct {\n\t\tre model1.RowEvent\n\t\te  tcell.Color\n\t}{\n\t\t\"add\": {\n\t\t\tmodel1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t},\n\t\t\tmodel1.AddColor,\n\t\t},\n\t\t\"update\": {\n\t\t\tmodel1.RowEvent{\n\t\t\t\tKind: model1.EventUpdate,\n\t\t\t},\n\t\t\tmodel1.ModColor,\n\t\t},\n\t\t\"delete\": {\n\t\t\tmodel1.RowEvent{\n\t\t\t\tKind: model1.EventDelete,\n\t\t\t},\n\t\t\tmodel1.KillColor,\n\t\t},\n\t\t\"no-change\": {\n\t\t\tmodel1.RowEvent{\n\t\t\t\tKind: model1.EventUnchanged,\n\t\t\t},\n\t\t\tmodel1.StdColor,\n\t\t},\n\t\t\"invalid\": {\n\t\t\tmodel1.RowEvent{\n\t\t\t\tKind: model1.EventUnchanged,\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"\", \"\", \"blah\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodel1.ErrColor,\n\t\t},\n\t}\n\n\th := model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\tmodel1.HeaderColumn{Name: \"B\"},\n\t\tmodel1.HeaderColumn{Name: \"VALID\"},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, model1.DefaultColorer(\"\", h, &u.re))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/model1/delta.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model1\n\nimport \"reflect\"\n\n// DeltaRow represents a collection of row deltas between old and new row.\ntype DeltaRow []string\n\n// NewDeltaRow computes the delta between 2 rows.\nfunc NewDeltaRow(o, n Row, h Header) DeltaRow {\n\tdeltas := make(DeltaRow, len(o.Fields))\n\tfor i, old := range o.Fields {\n\t\tif i >= len(n.Fields) {\n\t\t\tcontinue\n\t\t}\n\t\tif old != \"\" && old != n.Fields[i] && !h.IsTimeCol(i) {\n\t\t\tdeltas[i] = old\n\t\t}\n\t}\n\n\treturn deltas\n}\n\n// Labelize returns a new deltaRow based on labels.\nfunc (d DeltaRow) Labelize(cols []int, labelCol int) DeltaRow {\n\tif len(d) == 0 {\n\t\treturn d\n\t}\n\t_, vals := sortLabels(labelize(d[labelCol]))\n\tout := make(DeltaRow, 0, len(cols)+len(vals))\n\tfor _, i := range cols {\n\t\tout = append(out, d[i])\n\t}\n\tfor _, v := range vals {\n\t\tout = append(out, v)\n\t}\n\n\treturn out\n}\n\n// Diff returns true if deltas differ or false otherwise.\nfunc (d DeltaRow) Diff(r DeltaRow, ageCol int) bool {\n\tif len(d) != len(r) {\n\t\treturn true\n\t}\n\n\tif ageCol < 0 || ageCol >= len(d) {\n\t\treturn !reflect.DeepEqual(d, r)\n\t}\n\n\tif !reflect.DeepEqual(d[:ageCol], r[:ageCol]) {\n\t\treturn true\n\t}\n\tif ageCol+1 >= len(d) {\n\t\treturn false\n\t}\n\n\treturn !reflect.DeepEqual(d[ageCol+1:], r[ageCol+1:])\n}\n\n// Customize returns a subset of deltas.\nfunc (d DeltaRow) Customize(cols []int, out DeltaRow) {\n\tif d.IsBlank() {\n\t\treturn\n\t}\n\tfor i, c := range cols {\n\t\tif c < 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif c < len(d) && i < len(out) {\n\t\t\tout[i] = d[c]\n\t\t}\n\t}\n}\n\n// IsBlank asserts a row has no values in it.\nfunc (d DeltaRow) IsBlank() bool {\n\tif len(d) == 0 {\n\t\treturn true\n\t}\n\n\tfor _, v := range d {\n\t\tif v != \"\" {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\n// Clone returns a delta copy.\nfunc (d DeltaRow) Clone() DeltaRow {\n\tres := make(DeltaRow, len(d))\n\tcopy(res, d)\n\n\treturn res\n}\n"
  },
  {
    "path": "internal/model1/delta_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model1_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestDeltaLabelize(t *testing.T) {\n\tuu := map[string]struct {\n\t\to model1.Row\n\t\tn model1.Row\n\t\te model1.DeltaRow\n\t}{\n\t\t\"same\": {\n\t\t\to: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a\", \"b\", \"blee=fred,doh=zorg\"},\n\t\t\t},\n\t\t\tn: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a\", \"b\", \"blee=fred1,doh=zorg\"},\n\t\t\t},\n\t\t\te: model1.DeltaRow{\"\", \"\", \"fred\", \"zorg\"},\n\t\t},\n\t}\n\n\thh := model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\tmodel1.HeaderColumn{Name: \"B\"},\n\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t}\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\td := model1.NewDeltaRow(u.o, u.n, hh)\n\t\t\td = d.Labelize([]int{0, 1}, 2)\n\t\t\tassert.Equal(t, u.e, d)\n\t\t})\n\t}\n}\n\nfunc TestDeltaCustomize(t *testing.T) {\n\tuu := map[string]struct {\n\t\tr1, r2 model1.Row\n\t\tcols   []int\n\t\te      model1.DeltaRow\n\t}{\n\t\t\"same\": {\n\t\t\tr1: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a\", \"b\", \"c\"},\n\t\t\t},\n\t\t\tr2: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a\", \"b\", \"c\"},\n\t\t\t},\n\t\t\tcols: []int{0, 1, 2},\n\t\t\te:    model1.DeltaRow{\"\", \"\", \"\"},\n\t\t},\n\t\t\"empty\": {\n\t\t\tr1: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a\", \"b\", \"c\"},\n\t\t\t},\n\t\t\tr2: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a\", \"b\", \"c\"},\n\t\t\t},\n\t\t\te: model1.DeltaRow{},\n\t\t},\n\t\t\"diff-full\": {\n\t\t\tr1: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a\", \"b\", \"c\"},\n\t\t\t},\n\t\t\tr2: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a1\", \"b1\", \"c1\"},\n\t\t\t},\n\t\t\tcols: []int{0, 1, 2},\n\t\t\te:    model1.DeltaRow{\"a\", \"b\", \"c\"},\n\t\t},\n\t\t\"diff-reverse\": {\n\t\t\tr1: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a\", \"b\", \"c\"},\n\t\t\t},\n\t\t\tr2: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a1\", \"b1\", \"c1\"},\n\t\t\t},\n\t\t\tcols: []int{2, 1, 0},\n\t\t\te:    model1.DeltaRow{\"c\", \"b\", \"a\"},\n\t\t},\n\t\t\"diff-skip\": {\n\t\t\tr1: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a\", \"b\", \"c\"},\n\t\t\t},\n\t\t\tr2: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a1\", \"b1\", \"c1\"},\n\t\t\t},\n\t\t\tcols: []int{2, 0},\n\t\t\te:    model1.DeltaRow{\"c\", \"a\"},\n\t\t},\n\t\t\"diff-missing\": {\n\t\t\tr1: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a\", \"b\", \"c\"},\n\t\t\t},\n\t\t\tr2: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a1\", \"b1\", \"c1\"},\n\t\t\t},\n\t\t\tcols: []int{2, 10, 0},\n\t\t\te:    model1.DeltaRow{\"c\", \"\", \"a\"},\n\t\t},\n\t\t\"diff-negative\": {\n\t\t\tr1: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a\", \"b\", \"c\"},\n\t\t\t},\n\t\t\tr2: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a1\", \"b1\", \"c1\"},\n\t\t\t},\n\t\t\tcols: []int{2, -1, 0},\n\t\t\te:    model1.DeltaRow{\"c\", \"\", \"a\"},\n\t\t},\n\t}\n\n\thh := model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\tmodel1.HeaderColumn{Name: \"B\"},\n\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t}\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\td := model1.NewDeltaRow(u.r1, u.r2, hh)\n\t\t\tout := make(model1.DeltaRow, len(u.cols))\n\t\t\td.Customize(u.cols, out)\n\t\t\tassert.Equal(t, u.e, out)\n\t\t})\n\t}\n}\n\nfunc TestDeltaNew(t *testing.T) {\n\tuu := map[string]struct {\n\t\to     model1.Row\n\t\tn     model1.Row\n\t\tblank bool\n\t\te     model1.DeltaRow\n\t}{\n\t\t\"same\": {\n\t\t\to: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a\", \"b\", \"c\"},\n\t\t\t},\n\t\t\tn: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a\", \"b\", \"c\"},\n\t\t\t},\n\t\t\tblank: true,\n\t\t\te:     model1.DeltaRow{\"\", \"\", \"\"},\n\t\t},\n\t\t\"diff\": {\n\t\t\to: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a1\", \"b\", \"c\"},\n\t\t\t},\n\t\t\tn: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a\", \"b\", \"c\"},\n\t\t\t},\n\t\t\te: model1.DeltaRow{\"a1\", \"\", \"\"},\n\t\t},\n\t\t\"diff2\": {\n\t\t\to: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a\", \"b\", \"c\"},\n\t\t\t},\n\t\t\tn: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a\", \"b1\", \"c\"},\n\t\t\t},\n\t\t\te: model1.DeltaRow{\"\", \"b\", \"\"},\n\t\t},\n\t\t\"diffLast\": {\n\t\t\to: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a\", \"b\", \"c\"},\n\t\t\t},\n\t\t\tn: model1.Row{\n\t\t\t\tFields: model1.Fields{\"a\", \"b\", \"c1\"},\n\t\t\t},\n\t\t\te: model1.DeltaRow{\"\", \"\", \"c\"},\n\t\t},\n\t}\n\n\thh := model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\tmodel1.HeaderColumn{Name: \"B\"},\n\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t}\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\td := model1.NewDeltaRow(u.o, u.n, hh)\n\t\t\tassert.Equal(t, u.e, d)\n\t\t\tassert.Equal(t, u.blank, d.IsBlank())\n\t\t})\n\t}\n}\n\nfunc TestDeltaBlank(t *testing.T) {\n\tuu := map[string]struct {\n\t\tr model1.DeltaRow\n\t\te bool\n\t}{\n\t\t\"empty\": {\n\t\t\tr: model1.DeltaRow{},\n\t\t\te: true,\n\t\t},\n\t\t\"blank\": {\n\t\t\tr: model1.DeltaRow{\"\", \"\", \"\"},\n\t\t\te: true,\n\t\t},\n\t\t\"notblank\": {\n\t\t\tr: model1.DeltaRow{\"\", \"\", \"z\"},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.r.IsBlank())\n\t\t})\n\t}\n}\n\nfunc TestDeltaDiff(t *testing.T) {\n\tuu := map[string]struct {\n\t\td1, d2 model1.DeltaRow\n\t\tageCol int\n\t\te      bool\n\t}{\n\t\t\"empty\": {\n\t\t\td1:     model1.DeltaRow{\"f1\", \"f2\", \"f3\"},\n\t\t\tageCol: 2,\n\t\t\te:      true,\n\t\t},\n\t\t\"same\": {\n\t\t\td1:     model1.DeltaRow{\"f1\", \"f2\", \"f3\"},\n\t\t\td2:     model1.DeltaRow{\"f1\", \"f2\", \"f3\"},\n\t\t\tageCol: -1,\n\t\t},\n\t\t\"diff\": {\n\t\t\td1:     model1.DeltaRow{\"f1\", \"f2\", \"f3\"},\n\t\t\td2:     model1.DeltaRow{\"f1\", \"f2\", \"f13\"},\n\t\t\tageCol: -1,\n\t\t\te:      true,\n\t\t},\n\t\t\"diff-age-first\": {\n\t\t\td1:     model1.DeltaRow{\"f1\", \"f2\", \"f3\"},\n\t\t\td2:     model1.DeltaRow{\"f1\", \"f2\", \"f13\"},\n\t\t\tageCol: 0,\n\t\t\te:      true,\n\t\t},\n\t\t\"diff-age-last\": {\n\t\t\td1:     model1.DeltaRow{\"f1\", \"f2\", \"f3\"},\n\t\t\td2:     model1.DeltaRow{\"f1\", \"f2\", \"f13\"},\n\t\t\tageCol: 2,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.d1.Diff(u.d2, u.ageCol))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/model1/fields.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model1\n\nimport \"reflect\"\n\n// Fields represents a collection of row fields.\ntype Fields []string\n\n// Customize returns a subset of fields.\nfunc (f Fields) Customize(cols []int, out Fields) {\n\tfor i, c := range cols {\n\t\tif c < 0 {\n\t\t\tout[i] = NAValue\n\t\t\tcontinue\n\t\t}\n\t\tif c < len(f) {\n\t\t\tout[i] = f[c]\n\t\t}\n\t}\n}\n\n// Diff returns true if fields differ or false otherwise.\nfunc (f Fields) Diff(ff Fields, ageCol int) bool {\n\tif ageCol < 0 {\n\t\treturn !reflect.DeepEqual(f[:len(f)-1], ff[:len(ff)-1])\n\t}\n\tif !reflect.DeepEqual(f[:ageCol], ff[:ageCol]) {\n\t\treturn true\n\t}\n\treturn !reflect.DeepEqual(f[ageCol+1:], ff[ageCol+1:])\n}\n\n// Clone returns a copy of the fields.\nfunc (f Fields) Clone() Fields {\n\tcp := make(Fields, len(f))\n\tcopy(cp, f)\n\n\treturn cp\n}\n"
  },
  {
    "path": "internal/model1/header.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model1\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"reflect\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"k8s.io/apimachinery/pkg/util/sets\"\n)\n\nconst ageCol = \"AGE\"\n\ntype Attrs struct {\n\tAlign     int\n\tDecorator DecoratorFunc\n\tWide      bool\n\tShow      bool\n\tMX        bool\n\tMXC, MXM  bool\n\tTime      bool\n\tCapacity  bool\n\tVS        bool\n\tHide      bool\n}\n\nfunc (a Attrs) Merge(b Attrs) Attrs {\n\ta.MX = b.MX\n\ta.MXC = b.MXC\n\ta.MXM = b.MXM\n\ta.Decorator = b.Decorator\n\ta.VS = b.VS\n\n\tif a.Align == 0 {\n\t\ta.Align = b.Align\n\t}\n\n\tif !a.Hide {\n\t\ta.Hide = b.Hide\n\t}\n\tif !a.Show && !a.Wide {\n\t\ta.Wide = b.Wide\n\t}\n\n\tif !a.Time {\n\t\ta.Time = b.Time\n\t}\n\tif !a.Capacity {\n\t\ta.Capacity = b.Capacity\n\t}\n\n\treturn a\n}\n\n// HeaderColumn represent a table header.\ntype HeaderColumn struct {\n\tAttrs\n\tName string\n}\n\nfunc (h HeaderColumn) String() string {\n\treturn fmt.Sprintf(\"%s [%d::%t::%t::%t]\", h.Name, h.Align, h.Wide, h.MX, h.Time)\n}\n\n// Clone copies a header.\nfunc (h HeaderColumn) Clone() HeaderColumn {\n\treturn h\n}\n\n// ----------------------------------------------------------------------------\n\n// Header represents a table header.\ntype Header []HeaderColumn\n\nfunc (h Header) Clear() Header {\n\th = h[:0]\n\n\treturn h\n}\n\n// Clone duplicates a header.\nfunc (h Header) Clone() Header {\n\the := make(Header, 0, len(h))\n\tfor _, h := range h {\n\t\the = append(he, h.Clone())\n\t}\n\n\treturn he\n}\n\n// Labelize returns a new Header based on labels.\nfunc (h Header) Labelize(cols []int, labelCol int, rr *RowEvents) Header {\n\theader := make(Header, 0, len(cols)+1)\n\tfor _, c := range cols {\n\t\theader = append(header, h[c])\n\t}\n\tcc := rr.ExtractHeaderLabels(labelCol)\n\tfor _, c := range cc {\n\t\theader = append(header, HeaderColumn{Name: c})\n\t}\n\n\treturn header\n}\n\n// MapIndices returns a collection of mapped column indices based of the requested columns.\nfunc (h Header) MapIndices(cols []string, wide bool) []int {\n\tii := make([]int, 0, len(cols))\n\tcc := make(map[int]struct{}, len(cols))\n\tfor _, col := range cols {\n\t\tidx, ok := h.IndexOf(col, true)\n\t\tif !ok {\n\t\t\tslog.Warn(\"Column not found on resource\", slogs.ColName, col)\n\t\t}\n\t\tii, cc[idx] = append(ii, idx), struct{}{}\n\t}\n\tif !wide {\n\t\treturn ii\n\t}\n\n\tfor i := range h {\n\t\tif _, ok := cc[i]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tii = append(ii, i)\n\t}\n\treturn ii\n}\n\n// Customize builds a header from custom col definitions.\nfunc (h Header) Customize(cols []string, wide bool) Header {\n\tif len(cols) == 0 {\n\t\treturn h\n\t}\n\tcc := make(Header, 0, len(h))\n\txx := make(map[int]struct{}, len(h))\n\tfor _, c := range cols {\n\t\tidx, ok := h.IndexOf(c, true)\n\t\tif !ok {\n\t\t\tslog.Warn(\"Column is not available on this resource\", slogs.ColName, c)\n\t\t\tcc = append(cc, HeaderColumn{Name: c})\n\t\t\tcontinue\n\t\t}\n\t\txx[idx] = struct{}{}\n\t\tcol := h[idx].Clone()\n\t\tcol.Wide = false\n\t\tcc = append(cc, col)\n\t}\n\tif !wide {\n\t\treturn cc\n\t}\n\n\tfor i, c := range h {\n\t\tif _, ok := xx[i]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tcol := c.Clone()\n\t\tcol.Wide = true\n\t\tcc = append(cc, col)\n\t}\n\n\treturn cc\n}\n\n// Diff returns true if the header changed.\nfunc (h Header) Diff(header Header) bool {\n\tif len(h) != len(header) {\n\t\treturn true\n\t}\n\treturn !reflect.DeepEqual(h, header)\n}\n\n// FilterColIndices return viewable col header indices.\nfunc (h Header) FilterColIndices(ns string, wide bool) sets.Set[int] {\n\tif len(h) == 0 {\n\t\treturn nil\n\t}\n\tnsed := client.IsNamespaced(ns)\n\n\tcc := sets.New[int]()\n\tfor i, c := range h {\n\t\tif c.Name == \"AGE\" || !wide && c.Wide || c.Hide || (nsed && c.Name == \"NAMESPACE\") {\n\t\t\tcontinue\n\t\t}\n\t\tcc.Insert(i)\n\t}\n\n\treturn cc\n}\n\n// ColumnNames return header col names\nfunc (h Header) ColumnNames(wide bool) []string {\n\tif len(h) == 0 {\n\t\treturn nil\n\t}\n\tcc := make([]string, 0, len(h))\n\tfor _, c := range h {\n\t\tif !wide && c.Wide {\n\t\t\tcontinue\n\t\t}\n\t\tcc = append(cc, c.Name)\n\t}\n\n\treturn cc\n}\n\n// HasAge returns true if table has an age column.\nfunc (h Header) HasAge() bool {\n\t_, ok := h.IndexOf(ageCol, true)\n\n\treturn ok\n}\n\n// IsMetricsCol checks if given column index represents metrics.\nfunc (h Header) IsMetricsCol(col int) bool {\n\tif col < 0 || col >= len(h) {\n\t\treturn false\n\t}\n\n\treturn h[col].MX\n}\n\n// IsTimeCol checks if given column index represents a timestamp.\nfunc (h Header) IsTimeCol(col int) bool {\n\tif col < 0 || col >= len(h) {\n\t\treturn false\n\t}\n\n\treturn h[col].Time\n}\n\n// IsCapacityCol checks if given column index represents a capacity.\nfunc (h Header) IsCapacityCol(col int) bool {\n\tif col < 0 || col >= len(h) {\n\t\treturn false\n\t}\n\n\treturn h[col].Capacity\n}\n\n// IndexOf returns the col index or -1 if none.\nfunc (h Header) IndexOf(colName string, includeWide bool) (int, bool) {\n\tfor i, c := range h {\n\t\tif c.Wide && !includeWide {\n\t\t\tcontinue\n\t\t}\n\t\tif c.Name == colName {\n\t\t\treturn i, true\n\t\t}\n\t}\n\treturn -1, false\n}\n\n// Dump for debugging.\nfunc (h Header) Dump() {\n\tslog.Debug(\"HEADER\")\n\tfor i, c := range h {\n\t\tslog.Debug(fmt.Sprintf(\"%d %q -- %t\", i, c.Name, c.Wide))\n\t}\n}\n"
  },
  {
    "path": "internal/model1/header_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model1_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestHeaderMapIndices(t *testing.T) {\n\tuu := map[string]struct {\n\t\th1   model1.Header\n\t\tcols []string\n\t\twide bool\n\t\te    []int\n\t}{\n\t\t\"all\": {\n\t\t\th1:   makeHeader(),\n\t\t\tcols: []string{\"A\", \"B\", \"C\"},\n\t\t\te:    []int{0, 1, 2},\n\t\t},\n\t\t\"reverse\": {\n\t\t\th1:   makeHeader(),\n\t\t\tcols: []string{\"C\", \"B\", \"A\"},\n\t\t\te:    []int{2, 1, 0},\n\t\t},\n\t\t\"missing\": {\n\t\t\th1:   makeHeader(),\n\t\t\tcols: []string{\"Duh\", \"B\", \"A\"},\n\t\t\te:    []int{-1, 1, 0},\n\t\t},\n\t\t\"skip\": {\n\t\t\th1:   makeHeader(),\n\t\t\tcols: []string{\"C\", \"A\"},\n\t\t\te:    []int{2, 0},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tii := u.h1.MapIndices(u.cols, u.wide)\n\t\t\tassert.Equal(t, u.e, ii)\n\t\t})\n\t}\n}\n\nfunc TestHeaderIndexOf(t *testing.T) {\n\tuu := map[string]struct {\n\t\th        model1.Header\n\t\tname     string\n\t\twide, ok bool\n\t\te        int\n\t}{\n\t\t\"shown\": {\n\t\t\th:    makeHeader(),\n\t\t\tname: \"A\",\n\t\t\te:    0,\n\t\t\tok:   true,\n\t\t},\n\t\t\"hidden\": {\n\t\t\th:    makeHeader(),\n\t\t\tname: \"B\",\n\t\t\te:    -1,\n\t\t},\n\t\t\"hidden-wide\": {\n\t\t\th:    makeHeader(),\n\t\t\tname: \"B\",\n\t\t\twide: true,\n\t\t\te:    1,\n\t\t\tok:   true,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tidx, ok := u.h.IndexOf(u.name, u.wide)\n\t\t\tassert.Equal(t, u.ok, ok)\n\t\t\tassert.Equal(t, u.e, idx)\n\t\t})\n\t}\n}\n\nfunc TestHeaderCustomize(t *testing.T) {\n\tuu := map[string]struct {\n\t\th    model1.Header\n\t\tcols []string\n\t\twide bool\n\t\te    model1.Header\n\t}{\n\t\t\"default\": {\n\t\t\th: makeHeader(),\n\t\t\te: makeHeader(),\n\t\t},\n\t\t\"default-wide\": {\n\t\t\th:    makeHeader(),\n\t\t\twide: true,\n\t\t\te:    makeHeader(),\n\t\t},\n\t\t\"reverse\": {\n\t\t\th: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"B\", Attrs: model1.Attrs{Wide: true}},\n\t\t\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t\t\t},\n\t\t\tcols: []string{\"C\", \"A\"},\n\t\t\te: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\t\t},\n\t\t},\n\t\t\"reverse-wide\": {\n\t\t\th: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"B\", Attrs: model1.Attrs{Wide: true}},\n\t\t\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t\t\t},\n\t\t\tcols: []string{\"C\", \"A\"},\n\t\t\twide: true,\n\t\t\te: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"B\", Attrs: model1.Attrs{Wide: true}},\n\t\t\t},\n\t\t},\n\t\t\"toggle-wide\": {\n\t\t\th: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"B\", Attrs: model1.Attrs{Wide: true}},\n\t\t\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t\t\t},\n\t\t\tcols: []string{\"C\", \"B\"},\n\t\t\twide: true,\n\t\t\te: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"B\", Attrs: model1.Attrs{Wide: false}},\n\t\t\t\tmodel1.HeaderColumn{Name: \"A\", Attrs: model1.Attrs{Wide: true}},\n\t\t\t},\n\t\t},\n\t\t\"missing\": {\n\t\t\th: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"B\", Attrs: model1.Attrs{Wide: true}},\n\t\t\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t\t\t},\n\t\t\tcols: []string{\"BLEE\", \"A\"},\n\t\t\twide: true,\n\t\t\te: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"BLEE\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"B\", Attrs: model1.Attrs{Wide: true}},\n\t\t\t\tmodel1.HeaderColumn{Name: \"C\", Attrs: model1.Attrs{Wide: true}},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.h.Customize(u.cols, u.wide))\n\t\t})\n\t}\n}\n\nfunc TestHeaderDiff(t *testing.T) {\n\tuu := map[string]struct {\n\t\th1, h2 model1.Header\n\t\te      bool\n\t}{\n\t\t\"same\": {\n\t\t\th1: makeHeader(),\n\t\t\th2: makeHeader(),\n\t\t},\n\t\t\"size\": {\n\t\t\th1: makeHeader(),\n\t\t\th2: makeHeader()[1:],\n\t\t\te:  true,\n\t\t},\n\t\t\"differ-wide\": {\n\t\t\th1: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"B\", Attrs: model1.Attrs{Wide: true}},\n\t\t\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t\t\t},\n\t\t\th2: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"B\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t\t\t},\n\t\t\te: true,\n\t\t},\n\t\t\"differ-order\": {\n\t\t\th1: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"B\", Attrs: model1.Attrs{Wide: true}},\n\t\t\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t\t\t},\n\t\t\th2: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"B\", Attrs: model1.Attrs{Wide: true}},\n\t\t\t},\n\t\t\te: true,\n\t\t},\n\t\t\"differ-name\": {\n\t\t\th1: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\t\t},\n\t\t\th2: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"B\"},\n\t\t\t},\n\t\t\te: true,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.h1.Diff(u.h2))\n\t\t})\n\t}\n}\n\nfunc TestHeaderHasAge(t *testing.T) {\n\tuu := map[string]struct {\n\t\th      model1.Header\n\t\tage, e bool\n\t}{\n\t\t\"no-age\": {\n\t\t\th: model1.Header{},\n\t\t},\n\t\t\"age\": {\n\t\t\th: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"B\", Attrs: model1.Attrs{Wide: true}},\n\t\t\t\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n\t\t\t},\n\t\t\te:   true,\n\t\t\tage: true,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.h.HasAge())\n\t\t\tassert.Equal(t, u.e, u.h.IsTimeCol(2))\n\t\t})\n\t}\n}\n\nfunc TestHeaderColumns(t *testing.T) {\n\tuu := map[string]struct {\n\t\th    model1.Header\n\t\twide bool\n\t\te    []string\n\t}{\n\t\t\"empty\": {\n\t\t\th: model1.Header{},\n\t\t},\n\t\t\"regular\": {\n\t\t\th: makeHeader(),\n\t\t\te: []string{\"A\", \"C\"},\n\t\t},\n\t\t\"wide\": {\n\t\t\th:    makeHeader(),\n\t\t\te:    []string{\"A\", \"B\", \"C\"},\n\t\t\twide: true,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.h.ColumnNames(u.wide))\n\t\t})\n\t}\n}\n\nfunc TestHeaderClone(t *testing.T) {\n\tuu := map[string]struct {\n\t\th model1.Header\n\t}{\n\t\t\"empty\": {\n\t\t\th: model1.Header{},\n\t\t},\n\t\t\"full\": {\n\t\t\th: makeHeader(),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tc := u.h.Clone()\n\t\t\tassert.Len(t, u.h, len(c))\n\t\t\tif len(u.h) > 0 {\n\t\t\t\tu.h[0].Name = \"blee\"\n\t\t\t\tassert.Equal(t, \"A\", c[0].Name)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc makeHeader() model1.Header {\n\treturn model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\tmodel1.HeaderColumn{Name: \"B\", Attrs: model1.Attrs{Wide: true}},\n\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t}\n}\n"
  },
  {
    "path": "internal/model1/helpers.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model1\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"math\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/fvbommel/sortorder\"\n\t\"k8s.io/apimachinery/pkg/api/resource\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nconst poolSize = 10\n\nfunc Hydrate(ns string, oo []runtime.Object, rr Rows, re Renderer) error {\n\tpool := NewWorkerPool(context.Background(), poolSize)\n\tfor i, o := range oo {\n\t\tpool.Add(func(ctx context.Context) error {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tslog.Debug(\"Worker canceled\")\n\t\t\t\treturn nil\n\t\t\tdefault:\n\t\t\t\treturn re.Render(o, ns, &rr[i])\n\t\t\t}\n\t\t})\n\t}\n\terrs := pool.Drain()\n\tif len(errs) > 0 {\n\t\treturn errs[0]\n\t}\n\n\treturn nil\n}\n\nfunc GenericHydrate(ns string, table *metav1.Table, rr Rows, re Renderer) error {\n\tgr, ok := re.(Generic)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting generic renderer but got %T\", re)\n\t}\n\tgr.SetTable(ns, table)\n\tpool := NewWorkerPool(context.Background(), poolSize)\n\tfor i, row := range table.Rows {\n\t\tpool.Add(func(ctx context.Context) error {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tslog.Debug(\"Worker canceled\")\n\t\t\t\treturn nil\n\t\t\tdefault:\n\t\t\t\treturn gr.Render(row, ns, &rr[i])\n\t\t\t}\n\t\t})\n\t}\n\terrs := pool.Drain()\n\tif len(errs) > 0 {\n\t\treturn errs[0]\n\t}\n\n\treturn nil\n}\n\n// IsValid returns true if resource is valid, false otherwise.\nfunc IsValid(_ string, h Header, r Row) bool {\n\tif len(r.Fields) == 0 {\n\t\treturn true\n\t}\n\tidx, ok := h.IndexOf(\"VALID\", true)\n\tif !ok || idx >= len(r.Fields) {\n\t\treturn true\n\t}\n\n\treturn strings.TrimSpace(r.Fields[idx]) == \"\" || strings.ToLower(strings.TrimSpace(r.Fields[idx])) == \"true\"\n}\n\nfunc sortLabels(m map[string]string) (keys, vals []string) {\n\tfor k := range m {\n\t\tkeys = append(keys, k)\n\t}\n\tsort.Strings(keys)\n\tfor _, k := range keys {\n\t\tvals = append(vals, m[k])\n\t}\n\n\treturn\n}\n\n// Converts labels string to map.\nfunc labelize(labels string) map[string]string {\n\tll := strings.Split(labels, \",\")\n\tdata := make(map[string]string, len(ll))\n\tfor _, l := range ll {\n\t\ttokens := strings.Split(l, \"=\")\n\t\tif len(tokens) == 2 {\n\t\t\tdata[tokens[0]] = tokens[1]\n\t\t}\n\t}\n\n\treturn data\n}\n\nfunc durationToSeconds(duration string) int64 {\n\tif duration == \"\" {\n\t\treturn 0\n\t}\n\tif duration == NAValue {\n\t\treturn math.MaxInt64\n\t}\n\n\tnum := make([]rune, 0, 5)\n\tvar n, m int64\n\tfor _, r := range duration {\n\t\tswitch r {\n\t\tcase 'y':\n\t\t\tm = 365 * 24 * 60 * 60\n\t\tcase 'd':\n\t\t\tm = 24 * 60 * 60\n\t\tcase 'h':\n\t\t\tm = 60 * 60\n\t\tcase 'm':\n\t\t\tm = 60\n\t\tcase 's':\n\t\t\tm = 1\n\t\tdefault:\n\t\t\tnum = append(num, r)\n\t\t\tcontinue\n\t\t}\n\t\tn, num = n+runesToNum(num)*m, num[:0]\n\t}\n\n\treturn n\n}\n\nfunc runesToNum(rr []rune) int64 {\n\tvar r int64\n\tvar m int64 = 1\n\tfor i := len(rr) - 1; i >= 0; i-- {\n\t\tv := int64(rr[i] - '0')\n\t\tr += v * m\n\t\tm *= 10\n\t}\n\n\treturn r\n}\n\nfunc capacityToNumber(capacity string) int64 {\n\tif strings.TrimSpace(capacity) == \"\" {\n\t\treturn 0\n\t}\n\tquantity := resource.MustParse(capacity)\n\treturn quantity.Value()\n}\n\n// Less return true if c1 <= c2.\nfunc Less(isNumber, isDuration, isCapacity bool, id1, id2, v1, v2 string) bool {\n\tvar less bool\n\tswitch {\n\tcase isNumber:\n\t\tless = lessNumber(v1, v2)\n\tcase isDuration:\n\t\tless = lessDuration(v1, v2)\n\tcase isCapacity:\n\t\tless = lessCapacity(v1, v2)\n\tdefault:\n\t\tless = sortorder.NaturalLess(v1, v2)\n\t}\n\tif v1 == v2 {\n\t\treturn sortorder.NaturalLess(id1, id2)\n\t}\n\n\treturn less\n}\n\nfunc lessDuration(s1, s2 string) bool {\n\td1, d2 := durationToSeconds(s1), durationToSeconds(s2)\n\treturn d1 <= d2\n}\n\nfunc lessCapacity(s1, s2 string) bool {\n\tc1, c2 := capacityToNumber(s1), capacityToNumber(s2)\n\n\treturn c1 <= c2\n}\n\nfunc lessNumber(s1, s2 string) bool {\n\tv1, v2 := strings.ReplaceAll(s1, \",\", \"\"), strings.ReplaceAll(s2, \",\", \"\")\n\n\treturn sortorder.NaturalLess(v1, v2)\n}\n"
  },
  {
    "path": "internal/model1/helpers_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model1\n\nimport (\n\t\"math\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSortLabels(t *testing.T) {\n\tuu := map[string]struct {\n\t\tlabels string\n\t\te      [][]string\n\t}{\n\t\t\"simple\": {\n\t\t\tlabels: \"a=b,c=d\",\n\t\t\te: [][]string{\n\t\t\t\t{\"a\", \"c\"},\n\t\t\t\t{\"b\", \"d\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\thh, vv := sortLabels(labelize(u.labels))\n\t\t\tassert.Equal(t, u.e[0], hh)\n\t\t\tassert.Equal(t, u.e[1], vv)\n\t\t})\n\t}\n}\n\nfunc TestLabelize(t *testing.T) {\n\tuu := map[string]struct {\n\t\tlabels string\n\t\te      map[string]string\n\t}{\n\t\t\"simple\": {\n\t\t\tlabels: \"a=b,c=d\",\n\t\t\te:      map[string]string{\"a\": \"b\", \"c\": \"d\"},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, labelize(u.labels))\n\t\t})\n\t}\n}\n\nfunc TestIsValid(t *testing.T) {\n\tuu := map[string]struct {\n\t\tns string\n\t\th  Header\n\t\tr  Row\n\t\te  bool\n\t}{\n\t\t\"empty\": {\n\t\t\tns: \"blee\",\n\t\t\th:  Header{},\n\t\t\tr:  Row{},\n\t\t\te:  true,\n\t\t},\n\t\t\"valid\": {\n\t\t\tns: \"blee\",\n\t\t\th:  Header{HeaderColumn{Name: \"VALID\"}},\n\t\t\tr:  Row{Fields: Fields{\"true\"}},\n\t\t\te:  true,\n\t\t},\n\t\t\"invalid\": {\n\t\t\tns: \"blee\",\n\t\t\th:  Header{HeaderColumn{Name: \"VALID\"}},\n\t\t\tr:  Row{Fields: Fields{\"false\"}},\n\t\t\te:  false,\n\t\t},\n\t\t\"valid_capital_case\": {\n\t\t\tns: \"blee\",\n\t\t\th:  Header{HeaderColumn{Name: \"VALID\"}},\n\t\t\tr:  Row{Fields: Fields{\"True\"}},\n\t\t\te:  true,\n\t\t},\n\t\t\"valid_all_caps\": {\n\t\t\tns: \"blee\",\n\t\t\th:  Header{HeaderColumn{Name: \"VALID\"}},\n\t\t\tr:  Row{Fields: Fields{\"TRUE\"}},\n\t\t\te:  true,\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tvalid := IsValid(u.ns, u.h, u.r)\n\t\t\tassert.Equal(t, u.e, valid)\n\t\t})\n\t}\n}\n\nfunc TestDurationToSecond(t *testing.T) {\n\tuu := map[string]struct {\n\t\ts string\n\t\te int64\n\t}{\n\t\t\"seconds\":                 {s: \"22s\", e: 22},\n\t\t\"minutes\":                 {s: \"22m\", e: 1320},\n\t\t\"hours\":                   {s: \"12h\", e: 43200},\n\t\t\"days\":                    {s: \"3d\", e: 259200},\n\t\t\"day_hour\":                {s: \"3d9h\", e: 291600},\n\t\t\"day_hour_minute\":         {s: \"2d22h3m\", e: 252180},\n\t\t\"day_hour_minute_seconds\": {s: \"2d22h3m50s\", e: 252230},\n\t\t\"year\":                    {s: \"3y\", e: 94608000},\n\t\t\"year_day\":                {s: \"1y2d\", e: 31708800},\n\t\t\"n/a\":                     {s: NAValue, e: math.MaxInt64},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, durationToSeconds(u.s))\n\t\t})\n\t}\n}\n\nfunc TestCapacityToNumber(t *testing.T) {\n\tuu := map[string]struct {\n\t\ts string\n\t\te int64\n\t}{\n\t\t\"empty\": {s: \"\", e: 0},\n\t\t\"blank\": {s: \"  \", e: 0},\n\t\t\"1Gi\":   {s: \"1Gi\", e: 1073741824},\n\t\t\"10Mi\":  {s: \"10Mi\", e: 10485760},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, capacityToNumber(u.s))\n\t\t})\n\t}\n}\n\nfunc BenchmarkDurationToSecond(b *testing.B) {\n\tt := \"2d22h3m50s\"\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor range b.N {\n\t\tdurationToSeconds(t)\n\t}\n}\n"
  },
  {
    "path": "internal/model1/pool.go",
    "content": "package model1\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"sync\"\n\n\t\"github.com/derailed/k9s/internal/slogs\"\n)\n\ntype jobFn func(ctx context.Context) error\n\ntype WorkerPool struct {\n\tsemC     chan struct{}\n\terrC     chan error\n\tctx      context.Context\n\tcancelFn context.CancelFunc\n\tmx       sync.RWMutex\n\twg       sync.WaitGroup\n\twge      sync.WaitGroup\n\terrs     []error\n}\n\nfunc NewWorkerPool(ctx context.Context, size int) *WorkerPool {\n\t_, cancelFn := context.WithCancel(ctx)\n\n\tp := WorkerPool{\n\t\tsemC:     make(chan struct{}, size),\n\t\terrC:     make(chan error, 1),\n\t\tcancelFn: cancelFn,\n\t\tctx:      ctx,\n\t}\n\n\tp.wge.Add(1)\n\tgo func(wg *sync.WaitGroup) {\n\t\tdefer wg.Done()\n\t\tfor err := range p.errC {\n\t\t\tif err != nil {\n\t\t\t\tp.mx.Lock()\n\t\t\t\tp.errs = append(p.errs, err)\n\t\t\t\tp.mx.Unlock()\n\t\t\t}\n\t\t}\n\t}(&p.wge)\n\n\treturn &p\n}\n\nfunc (p *WorkerPool) Add(job jobFn) {\n\tp.semC <- struct{}{}\n\tp.wg.Add(1)\n\tgo func(ctx context.Context, wg *sync.WaitGroup, semC <-chan struct{}, errC chan<- error) {\n\t\tdefer func() {\n\t\t\t<-semC\n\t\t\twg.Done()\n\t\t}()\n\t\tif err := job(ctx); err != nil {\n\t\t\tslog.Error(\"Worker error\", slogs.Error, err)\n\t\t\terrC <- err\n\t\t}\n\t}(p.ctx, &p.wg, p.semC, p.errC)\n}\n\nfunc (p *WorkerPool) Drain() []error {\n\tif p.cancelFn != nil {\n\t\tp.cancelFn()\n\t\tp.cancelFn = nil\n\t}\n\tp.wg.Wait()\n\tclose(p.semC)\n\tclose(p.errC)\n\tp.wge.Wait()\n\n\tp.mx.RLock()\n\tdefer p.mx.RUnlock()\n\treturn p.errs\n}\n"
  },
  {
    "path": "internal/model1/pool_test.go",
    "content": "package model1_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync/atomic\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestWorkerPoolPlain(t *testing.T) {\n\tp := model1.NewWorkerPool(context.Background(), 2)\n\n\tvar c atomic.Int32\n\tfor range 10 {\n\t\tp.Add(func(ctx context.Context) error {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tfmt.Println(\"Worker canceled\")\n\t\t\t\treturn nil\n\t\t\tdefault:\n\t\t\t\tc.Add(1)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t})\n\t}\n\terrs := p.Drain()\n\tassert.Equal(t, 10, int(c.Load()))\n\tassert.Empty(t, errs)\n}\n\nfunc TestWorkerPoolWithError(t *testing.T) {\n\tctx := context.Background()\n\tp := model1.NewWorkerPool(ctx, 2)\n\n\tvar c atomic.Int32\n\tfor i := range 10 {\n\t\tp.Add(func(ctx context.Context) error {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tfmt.Println(\"Worker canceled\")\n\t\t\t\treturn nil\n\t\t\tdefault:\n\t\t\t\tif i%2 == 0 {\n\t\t\t\t\treturn fmt.Errorf(\"BOOM%d\", i)\n\t\t\t\t}\n\t\t\t\tc.Add(1)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t})\n\t}\n\terrs := p.Drain()\n\tassert.Equal(t, 5, int(c.Load()))\n\tassert.Len(t, errs, 5)\n}\n"
  },
  {
    "path": "internal/model1/row.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model1\n\n// Row represents a collection of columns.\ntype Row struct {\n\tID     string\n\tFields Fields\n}\n\n// NewRow returns a new row with initialized fields.\nfunc NewRow(size int) Row {\n\treturn Row{Fields: make([]string, size)}\n}\n\n// Labelize returns a new row based on labels.\nfunc (r Row) Labelize(cols []int, labelCol int, labels []string) Row {\n\tout := NewRow(len(cols) + len(labels))\n\tfor _, col := range cols {\n\t\tout.Fields = append(out.Fields, r.Fields[col])\n\t}\n\tm := labelize(r.Fields[labelCol])\n\tfor _, label := range labels {\n\t\tout.Fields = append(out.Fields, m[label])\n\t}\n\n\treturn out\n}\n\n// Customize returns a row subset based on given col indices.\nfunc (r Row) Customize(cols []int) Row {\n\tout := NewRow(len(cols))\n\tr.Fields.Customize(cols, out.Fields)\n\tout.ID = r.ID\n\n\treturn out\n}\n\n// Diff returns true if row differ or false otherwise.\nfunc (r Row) Diff(ro Row, ageCol int) bool {\n\tif r.ID != ro.ID {\n\t\treturn true\n\t}\n\treturn r.Fields.Diff(ro.Fields, ageCol)\n}\n\n// Clone copies a row.\nfunc (r Row) Clone() Row {\n\treturn Row{\n\t\tID:     r.ID,\n\t\tFields: r.Fields.Clone(),\n\t}\n}\n\n// Len returns the length of the row.\nfunc (r Row) Len() int {\n\treturn len(r.Fields)\n}\n\n// ----------------------------------------------------------------------------\n\n// RowSorter sorts rows.\ntype RowSorter struct {\n\tRows       Rows\n\tIndex      int\n\tIsNumber   bool\n\tIsDuration bool\n\tIsCapacity bool\n\tAsc        bool\n}\n\nfunc (s RowSorter) Len() int {\n\treturn len(s.Rows)\n}\n\nfunc (s RowSorter) Swap(i, j int) {\n\ts.Rows[i], s.Rows[j] = s.Rows[j], s.Rows[i]\n}\n\nfunc (s RowSorter) Less(i, j int) bool {\n\tv1, v2 := s.Rows[i].Fields[s.Index], s.Rows[j].Fields[s.Index]\n\tid1, id2 := s.Rows[i].ID, s.Rows[j].ID\n\tless := Less(s.IsNumber, s.IsDuration, s.IsCapacity, id1, id2, v1, v2)\n\tif s.Asc {\n\t\treturn less\n\t}\n\treturn !less\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n"
  },
  {
    "path": "internal/model1/row_event.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model1\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sort\"\n)\n\ntype ReRangeFn func(int, RowEvent) bool\n\n// ResEvent represents a resource event.\ntype ResEvent int\n\n// RowEvent tracks resource instance events.\ntype RowEvent struct {\n\tKind   ResEvent\n\tRow    Row\n\tDeltas DeltaRow\n}\n\n// NewRowEvent returns a new row event.\nfunc NewRowEvent(kind ResEvent, row Row) RowEvent {\n\treturn RowEvent{\n\t\tKind: kind,\n\t\tRow:  row,\n\t}\n}\n\n// NewRowEventWithDeltas returns a new row event with deltas.\nfunc NewRowEventWithDeltas(row Row, delta DeltaRow) RowEvent {\n\treturn RowEvent{\n\t\tKind:   EventUpdate,\n\t\tRow:    row,\n\t\tDeltas: delta,\n\t}\n}\n\n// Clone returns a row event deep copy.\nfunc (r RowEvent) Clone() RowEvent {\n\treturn RowEvent{\n\t\tKind:   r.Kind,\n\t\tRow:    r.Row.Clone(),\n\t\tDeltas: r.Deltas.Clone(),\n\t}\n}\n\n// Customize returns a new subset based on the given column indices.\nfunc (r RowEvent) Customize(cols []int) RowEvent {\n\tdelta := r.Deltas\n\tif !r.Deltas.IsBlank() {\n\t\tdelta = make(DeltaRow, len(cols))\n\t\tr.Deltas.Customize(cols, delta)\n\t}\n\n\treturn RowEvent{\n\t\tKind:   r.Kind,\n\t\tDeltas: delta,\n\t\tRow:    r.Row.Customize(cols),\n\t}\n}\n\n// ExtractHeaderLabels extract collection of fields into header.\nfunc (r RowEvent) ExtractHeaderLabels(labelCol int) []string {\n\thh, _ := sortLabels(labelize(r.Row.Fields[labelCol]))\n\treturn hh\n}\n\n// Labelize returns a new row event based on labels.\nfunc (r RowEvent) Labelize(cols []int, labelCol int, labels []string) RowEvent {\n\treturn RowEvent{\n\t\tKind:   r.Kind,\n\t\tDeltas: r.Deltas.Labelize(cols, labelCol),\n\t\tRow:    r.Row.Labelize(cols, labelCol, labels),\n\t}\n}\n\n// Diff returns true if the row changed.\nfunc (r RowEvent) Diff(re RowEvent, ageCol int) bool {\n\tif r.Kind != re.Kind {\n\t\treturn true\n\t}\n\tif r.Deltas.Diff(re.Deltas, ageCol) {\n\t\treturn true\n\t}\n\treturn r.Row.Diff(re.Row, ageCol)\n}\n\n// ----------------------------------------------------------------------------\n\ntype reIndex map[string]int\n\n// RowEvents a collection of row events.\ntype RowEvents struct {\n\tevents []RowEvent\n\tindex  reIndex\n}\n\nfunc NewRowEvents(size int) *RowEvents {\n\treturn &RowEvents{\n\t\tevents: make([]RowEvent, 0, size),\n\t\tindex:  make(reIndex, size),\n\t}\n}\n\nfunc NewRowEventsWithEvts(ee ...RowEvent) *RowEvents {\n\tre := NewRowEvents(len(ee))\n\tfor _, e := range ee {\n\t\tre.Add(e)\n\t}\n\n\treturn re\n}\n\nfunc (r *RowEvents) reindex() {\n\tfor i, e := range r.events {\n\t\tr.index[e.Row.ID] = i\n\t}\n}\n\nfunc (r *RowEvents) At(i int) (RowEvent, bool) {\n\tif i < 0 || i > len(r.events) {\n\t\treturn RowEvent{}, false\n\t}\n\n\treturn r.events[i], true\n}\n\nfunc (r *RowEvents) Set(i int, re RowEvent) {\n\tr.events[i] = re\n\tr.index[re.Row.ID] = i\n}\n\nfunc (r *RowEvents) Add(re RowEvent) {\n\tr.events = append(r.events, re)\n\tr.index[re.Row.ID] = len(r.events) - 1\n}\n\n// ExtractHeaderLabels extract header labels.\nfunc (r *RowEvents) ExtractHeaderLabels(labelCol int) []string {\n\tll := make([]string, 0, 10)\n\tfor _, re := range r.events {\n\t\tll = append(ll, re.ExtractHeaderLabels(labelCol)...)\n\t}\n\n\treturn ll\n}\n\n// Labelize converts labels into a row event.\nfunc (r *RowEvents) Labelize(cols []int, labelCol int, labels []string) *RowEvents {\n\tout := make([]RowEvent, 0, len(r.events))\n\tfor _, re := range r.events {\n\t\tout = append(out, re.Labelize(cols, labelCol, labels))\n\t}\n\n\treturn NewRowEventsWithEvts(out...)\n}\n\n// Customize returns custom row events based on columns layout.\nfunc (r *RowEvents) Customize(cols []int) *RowEvents {\n\tee := make([]RowEvent, 0, len(cols))\n\tfor _, re := range r.events {\n\t\tee = append(ee, re.Customize(cols))\n\t}\n\n\treturn NewRowEventsWithEvts(ee...)\n}\n\n// Diff returns true if the event changed.\nfunc (r *RowEvents) Diff(re *RowEvents, ageCol int) bool {\n\tif len(r.events) != len(re.events) {\n\t\treturn true\n\t}\n\tfor i := range r.events {\n\t\tif r.events[i].Diff(re.events[i], ageCol) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// Clone returns a deep copy.\nfunc (r *RowEvents) Clone() *RowEvents {\n\tre := make([]RowEvent, 0, len(r.events))\n\tfor _, e := range r.events {\n\t\tre = append(re, e.Clone())\n\t}\n\n\treturn NewRowEventsWithEvts(re...)\n}\n\n// Upsert add or update a row if it exists.\nfunc (r *RowEvents) Upsert(re RowEvent) {\n\tif idx, ok := r.FindIndex(re.Row.ID); ok {\n\t\tr.events[idx] = re\n\t} else {\n\t\tr.Add(re)\n\t}\n}\n\n// Delete removes an element by id.\nfunc (r *RowEvents) Delete(fqn string) error {\n\tvictim, ok := r.FindIndex(fqn)\n\tif !ok {\n\t\treturn fmt.Errorf(\"unable to delete row with fqn: %q\", fqn)\n\t}\n\tr.events = append(r.events[0:victim], r.events[victim+1:]...)\n\tdelete(r.index, fqn)\n\tr.reindex()\n\n\treturn nil\n}\n\nfunc (r *RowEvents) Len() int {\n\treturn len(r.events)\n}\n\nfunc (r *RowEvents) Empty() bool {\n\treturn len(r.events) == 0\n}\n\n// Clear delete all row events.\nfunc (r *RowEvents) Clear() {\n\tr.events = r.events[:0]\n\tfor k := range r.index {\n\t\tdelete(r.index, k)\n\t}\n}\n\nfunc (r *RowEvents) Range(f ReRangeFn) {\n\tfor i, e := range r.events {\n\t\tif !f(i, e) {\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (r *RowEvents) Get(id string) (RowEvent, bool) {\n\ti, ok := r.index[id]\n\tif !ok {\n\t\treturn RowEvent{}, false\n\t}\n\n\treturn r.At(i)\n}\n\n// FindIndex locates a row index by id. Returns false is not found.\nfunc (r *RowEvents) FindIndex(id string) (int, bool) {\n\ti, ok := r.index[id]\n\n\treturn i, ok\n}\n\n// Sort rows based on column index and order.\nfunc (r *RowEvents) Sort(ns string, sortCol int, isDuration, numCol, isCapacity, asc bool) {\n\tif sortCol == -1 || r == nil {\n\t\treturn\n\t}\n\n\tt := RowEventSorter{\n\t\tNS:         ns,\n\t\tEvents:     r,\n\t\tIndex:      sortCol,\n\t\tAsc:        asc,\n\t\tIsNumber:   numCol,\n\t\tIsDuration: isDuration,\n\t\tIsCapacity: isCapacity,\n\t}\n\tsort.Sort(t)\n\tr.reindex()\n}\n\n// For debugging...\nfunc (re RowEvents) Dump(msg string) {\n\tslog.Debug(\"[DEBUG] RowEvents\" + msg)\n\tfor _, r := range re.events {\n\t\tslog.Debug(fmt.Sprintf(\"   %#v\", r))\n\t}\n}\n\n// ----------------------------------------------------------------------------\n\n// RowEventSorter sorts row events by a given colon.\ntype RowEventSorter struct {\n\tEvents     *RowEvents\n\tIndex      int\n\tNS         string\n\tIsNumber   bool\n\tIsDuration bool\n\tIsCapacity bool\n\tAsc        bool\n}\n\nfunc (r RowEventSorter) Len() int {\n\treturn len(r.Events.events)\n}\n\nfunc (r RowEventSorter) Swap(i, j int) {\n\tr.Events.events[i], r.Events.events[j] = r.Events.events[j], r.Events.events[i]\n}\n\nfunc (r RowEventSorter) Less(i, j int) bool {\n\tf1, f2 := r.Events.events[i].Row.Fields, r.Events.events[j].Row.Fields\n\tid1, id2 := r.Events.events[i].Row.ID, r.Events.events[j].Row.ID\n\tless := Less(r.IsNumber, r.IsDuration, r.IsCapacity, id1, id2, f1[r.Index], f2[r.Index])\n\tif r.Asc {\n\t\treturn less\n\t}\n\n\treturn !less\n}\n"
  },
  {
    "path": "internal/model1/row_event_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model1_test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRowEventCustomize(t *testing.T) {\n\tuu := map[string]struct {\n\t\tre1, e model1.RowEvent\n\t\tcols   []int\n\t}{\n\t\t\"empty\": {\n\t\t\tre1: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow:  model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}},\n\t\t\t},\n\t\t\te: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow:  model1.Row{ID: \"A\", Fields: model1.Fields{}},\n\t\t\t},\n\t\t},\n\t\t\"full\": {\n\t\t\tre1: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow:  model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}},\n\t\t\t},\n\t\t\te: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow:  model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}},\n\t\t\t},\n\t\t\tcols: []int{0, 1, 2},\n\t\t},\n\t\t\"deltas\": {\n\t\t\tre1: model1.RowEvent{\n\t\t\t\tKind:   model1.EventAdd,\n\t\t\t\tRow:    model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}},\n\t\t\t\tDeltas: model1.DeltaRow{\"a\", \"b\", \"c\"},\n\t\t\t},\n\t\t\te: model1.RowEvent{\n\t\t\t\tKind:   model1.EventAdd,\n\t\t\t\tRow:    model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}},\n\t\t\t\tDeltas: model1.DeltaRow{\"a\", \"b\", \"c\"},\n\t\t\t},\n\t\t\tcols: []int{0, 1, 2},\n\t\t},\n\t\t\"deltas-skip\": {\n\t\t\tre1: model1.RowEvent{\n\t\t\t\tKind:   model1.EventAdd,\n\t\t\t\tRow:    model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}},\n\t\t\t\tDeltas: model1.DeltaRow{\"a\", \"b\", \"c\"},\n\t\t\t},\n\t\t\te: model1.RowEvent{\n\t\t\t\tKind:   model1.EventAdd,\n\t\t\t\tRow:    model1.Row{ID: \"A\", Fields: model1.Fields{\"3\", \"1\"}},\n\t\t\t\tDeltas: model1.DeltaRow{\"c\", \"a\"},\n\t\t\t},\n\t\t\tcols: []int{2, 0},\n\t\t},\n\t\t\"reverse\": {\n\t\t\tre1: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow:  model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}},\n\t\t\t},\n\t\t\te: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow:  model1.Row{ID: \"A\", Fields: model1.Fields{\"3\", \"2\", \"1\"}},\n\t\t\t},\n\t\t\tcols: []int{2, 1, 0},\n\t\t},\n\t\t\"skip\": {\n\t\t\tre1: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow:  model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}},\n\t\t\t},\n\t\t\te: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow:  model1.Row{ID: \"A\", Fields: model1.Fields{\"3\", \"1\"}},\n\t\t\t},\n\t\t\tcols: []int{2, 0},\n\t\t},\n\t\t\"miss\": {\n\t\t\tre1: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow:  model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}},\n\t\t\t},\n\t\t\te: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow:  model1.Row{ID: \"A\", Fields: model1.Fields{\"3\", \"\", \"1\"}},\n\t\t\t},\n\t\t\tcols: []int{2, 10, 0},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.re1.Customize(u.cols))\n\t\t})\n\t}\n}\n\nfunc TestRowEventDiff(t *testing.T) {\n\tuu := map[string]struct {\n\t\tre1, re2 model1.RowEvent\n\t\te        bool\n\t}{\n\t\t\"same\": {\n\t\t\tre1: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow:  model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}},\n\t\t\t},\n\t\t\tre2: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow:  model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}},\n\t\t\t},\n\t\t},\n\t\t\"diff-kind\": {\n\t\t\tre1: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow:  model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}},\n\t\t\t},\n\t\t\tre2: model1.RowEvent{\n\t\t\t\tKind: model1.EventDelete,\n\t\t\t\tRow:  model1.Row{ID: \"B\", Fields: model1.Fields{\"1\", \"2\", \"3\"}},\n\t\t\t},\n\t\t\te: true,\n\t\t},\n\t\t\"diff-delta\": {\n\t\t\tre1: model1.RowEvent{\n\t\t\t\tKind:   model1.EventAdd,\n\t\t\t\tRow:    model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}},\n\t\t\t\tDeltas: model1.DeltaRow{\"1\", \"2\", \"3\"},\n\t\t\t},\n\t\t\tre2: model1.RowEvent{\n\t\t\t\tKind:   model1.EventAdd,\n\t\t\t\tRow:    model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}},\n\t\t\t\tDeltas: model1.DeltaRow{\"10\", \"2\", \"3\"},\n\t\t\t},\n\t\t\te: true,\n\t\t},\n\t\t\"diff-id\": {\n\t\t\tre1: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow:  model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}},\n\t\t\t},\n\t\t\tre2: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow:  model1.Row{ID: \"B\", Fields: model1.Fields{\"1\", \"2\", \"3\"}},\n\t\t\t},\n\t\t\te: true,\n\t\t},\n\t\t\"diff-field\": {\n\t\t\tre1: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow:  model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}},\n\t\t\t},\n\t\t\tre2: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow:  model1.Row{ID: \"A\", Fields: model1.Fields{\"10\", \"2\", \"3\"}},\n\t\t\t},\n\t\t\te: true,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.re1.Diff(u.re2, -1))\n\t\t})\n\t}\n}\n\nfunc TestRowEventsDiff(t *testing.T) {\n\tuu := map[string]struct {\n\t\tre1, re2 *model1.RowEvents\n\t\tageCol   int\n\t\te        bool\n\t}{\n\t\t\"same\": {\n\t\t\tre1: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tre2: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tageCol: -1,\n\t\t},\n\t\t\"diff-len\": {\n\t\t\tre1: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tre2: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tageCol: -1,\n\t\t\te:      true,\n\t\t},\n\t\t\"diff-id\": {\n\t\t\tre1: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tre2: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"D\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tageCol: -1,\n\t\t\te:      true,\n\t\t},\n\t\t\"diff-order\": {\n\t\t\tre1: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tre2: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tageCol: -1,\n\t\t\te:      true,\n\t\t},\n\t\t\"diff-withAge\": {\n\t\t\tre1: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tre2: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"13\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tageCol: 1,\n\t\t\te:      true,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.re1.Diff(u.re2, u.ageCol))\n\t\t})\n\t}\n}\n\nfunc TestRowEventsUpsert(t *testing.T) {\n\tuu := map[string]struct {\n\t\tee, e *model1.RowEvents\n\t\tre    model1.RowEvent\n\t}{\n\t\t\"add\": {\n\t\t\tee: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tre: model1.RowEvent{\n\t\t\t\tRow: model1.Row{ID: \"D\", Fields: model1.Fields{\"f1\", \"f2\", \"f3\"}},\n\t\t\t},\n\t\t\te: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"D\", Fields: model1.Fields{\"f1\", \"f2\", \"f3\"}}},\n\t\t\t),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tu.ee.Upsert(u.re)\n\t\t\tassert.Equal(t, u.e, u.ee)\n\t\t})\n\t}\n}\n\nfunc TestRowEventsCustomize(t *testing.T) {\n\tuu := map[string]struct {\n\t\tre, e *model1.RowEvents\n\t\tcols  []int\n\t}{\n\t\t\"same\": {\n\t\t\tre: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tcols: []int{0, 1, 2},\n\t\t\te: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t},\n\t\t\"reverse\": {\n\t\t\tre: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tcols: []int{2, 1, 0},\n\t\t\te: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"3\", \"2\", \"1\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"3\", \"2\", \"0\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"3\", \"2\", \"10\"}}},\n\t\t\t),\n\t\t},\n\t\t\"skip\": {\n\t\t\tre: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tcols: []int{1, 0},\n\t\t\te: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"2\", \"1\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"2\", \"0\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"2\", \"10\"}}},\n\t\t\t),\n\t\t},\n\t\t\"missing\": {\n\t\t\tre: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tcols: []int{1, 0, 4},\n\t\t\te: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"2\", \"1\", \"\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"2\", \"0\", \"\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"2\", \"10\", \"\"}}},\n\t\t\t),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.re.Customize(u.cols))\n\t\t})\n\t}\n}\n\nfunc TestRowEventsDelete(t *testing.T) {\n\tuu := map[string]struct {\n\t\tre, e *model1.RowEvents\n\t\tid    string\n\t}{\n\t\t\"first\": {\n\t\t\tre: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tid: \"A\",\n\t\t\te: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t},\n\t\t\"middle\": {\n\t\t\tre: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tid: \"B\",\n\t\t\te: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t},\n\t\t\"last\": {\n\t\t\tre: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tid: \"C\",\n\t\t\te: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\trequire.NoError(t, u.re.Delete(u.id))\n\t\t\tassert.Equal(t, u.e, u.re)\n\t\t})\n\t}\n}\n\nfunc TestRowEventsSort(t *testing.T) {\n\tuu := map[string]struct {\n\t\tre, e              *model1.RowEvents\n\t\tcol                int\n\t\tduration, num, asc bool\n\t\tcapacity           bool\n\t}{\n\t\t\"age_time\": {\n\t\t\tre: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", testTime().Add(20 * time.Second).String()}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", testTime().Add(10 * time.Second).String()}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", testTime().String()}}},\n\t\t\t),\n\t\t\tcol:      2,\n\t\t\tasc:      true,\n\t\t\tduration: true,\n\t\t\te: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", testTime().String()}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", testTime().Add(10 * time.Second).String()}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", testTime().Add(20 * time.Second).String()}}},\n\t\t\t),\n\t\t},\n\t\t\"col0\": {\n\t\t\tre: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tcol: 0,\n\t\t\tasc: true,\n\t\t\te: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"B\", Fields: model1.Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"A\", Fields: model1.Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"C\", Fields: model1.Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t},\n\t\t\"id_preserve\": {\n\t\t\tre: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns1/B\", Fields: model1.Fields{\"B\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns1/A\", Fields: model1.Fields{\"A\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns1/C\", Fields: model1.Fields{\"C\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns2/B\", Fields: model1.Fields{\"B\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns2/A\", Fields: model1.Fields{\"A\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns2/C\", Fields: model1.Fields{\"C\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tcol: 1,\n\t\t\tasc: true,\n\t\t\te: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns1/A\", Fields: model1.Fields{\"A\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns1/B\", Fields: model1.Fields{\"B\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns1/C\", Fields: model1.Fields{\"C\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns2/A\", Fields: model1.Fields{\"A\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns2/B\", Fields: model1.Fields{\"B\", \"2\", \"3\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns2/C\", Fields: model1.Fields{\"C\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t},\n\t\t\"capacity\": {\n\t\t\tre: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns1/B\", Fields: model1.Fields{\"B\", \"2\", \"3\", \"1Gi\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns1/A\", Fields: model1.Fields{\"A\", \"2\", \"3\", \"1.1G\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns1/C\", Fields: model1.Fields{\"C\", \"2\", \"3\", \"0.5Ti\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns2/B\", Fields: model1.Fields{\"B\", \"2\", \"3\", \"12e6\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns2/A\", Fields: model1.Fields{\"A\", \"2\", \"3\", \"1234\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns2/C\", Fields: model1.Fields{\"C\", \"2\", \"3\", \"0.1Ei\"}}},\n\t\t\t),\n\t\t\tcol:      3,\n\t\t\tasc:      true,\n\t\t\tcapacity: true,\n\t\t\te: model1.NewRowEventsWithEvts(\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns2/A\", Fields: model1.Fields{\"A\", \"2\", \"3\", \"1234\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns2/B\", Fields: model1.Fields{\"B\", \"2\", \"3\", \"12e6\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns1/B\", Fields: model1.Fields{\"B\", \"2\", \"3\", \"1Gi\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns1/A\", Fields: model1.Fields{\"A\", \"2\", \"3\", \"1.1G\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns1/C\", Fields: model1.Fields{\"C\", \"2\", \"3\", \"0.5Ti\"}}},\n\t\t\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns2/C\", Fields: model1.Fields{\"C\", \"2\", \"3\", \"0.1Ei\"}}},\n\t\t\t),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tu.re.Sort(\"\", u.col, u.duration, u.num, u.capacity, u.asc)\n\t\t\tassert.Equal(t, u.e, u.re)\n\t\t})\n\t}\n}\n\nfunc TestRowEventsClone(t *testing.T) {\n\tuu := map[string]struct {\n\t\tr *model1.RowEvents\n\t}{\n\t\t\"empty\": {\n\t\t\tr: model1.NewRowEventsWithEvts(),\n\t\t},\n\t\t\"full\": {\n\t\t\tr: makeRowEvents(),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tc := u.r.Clone()\n\t\t\tassert.Equal(t, u.r.Len(), c.Len())\n\t\t\tif !u.r.Empty() {\n\t\t\t\tr, ok := u.r.At(0)\n\t\t\t\tassert.True(t, ok)\n\t\t\t\tr.Row.Fields[0] = \"blee\"\n\t\t\t\tcr, ok := c.At(0)\n\t\t\t\tassert.True(t, ok)\n\t\t\t\tassert.Equal(t, \"A\", cr.Row.Fields[0])\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Helpers...\n\nfunc makeRowEvents() *model1.RowEvents {\n\treturn model1.NewRowEventsWithEvts(\n\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns1/A\", Fields: model1.Fields{\"A\", \"2\", \"3\"}}},\n\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns1/B\", Fields: model1.Fields{\"B\", \"2\", \"3\"}}},\n\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns1/C\", Fields: model1.Fields{\"C\", \"2\", \"3\"}}},\n\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns2/A\", Fields: model1.Fields{\"A\", \"2\", \"3\"}}},\n\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns2/B\", Fields: model1.Fields{\"B\", \"2\", \"3\"}}},\n\t\tmodel1.RowEvent{Row: model1.Row{ID: \"ns2/C\", Fields: model1.Fields{\"C\", \"2\", \"3\"}}},\n\t)\n}\n"
  },
  {
    "path": "internal/model1/row_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model1_test\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc BenchmarkRowCustomize(b *testing.B) {\n\trow := model1.Row{ID: \"fred\", Fields: model1.Fields{\"f1\", \"f2\", \"f3\"}}\n\tcols := []int{0, 1, 2}\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor range b.N {\n\t\t_ = row.Customize(cols)\n\t}\n}\n\nfunc TestFieldCustomize(t *testing.T) {\n\tuu := map[string]struct {\n\t\tfields model1.Fields\n\t\tcols   []int\n\t\te      model1.Fields\n\t}{\n\t\t\"empty\": {\n\t\t\tfields: model1.Fields{},\n\t\t\tcols:   []int{0, 1, 2},\n\t\t\te:      model1.Fields{\"\", \"\", \"\"},\n\t\t},\n\t\t\"no-cols\": {\n\t\t\tfields: model1.Fields{\"f1\", \"f2\", \"f3\"},\n\t\t\tcols:   []int{},\n\t\t\te:      model1.Fields{},\n\t\t},\n\t\t\"reverse\": {\n\t\t\tfields: model1.Fields{\"f1\", \"f2\", \"f3\"},\n\t\t\tcols:   []int{1, 0},\n\t\t\te:      model1.Fields{\"f2\", \"f1\"},\n\t\t},\n\t\t\"missing\": {\n\t\t\tfields: model1.Fields{\"f1\", \"f2\", \"f3\"},\n\t\t\tcols:   []int{10, 0},\n\t\t\te:      model1.Fields{\"\", \"f1\"},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tff := make(model1.Fields, len(u.cols))\n\t\t\tu.fields.Customize(u.cols, ff)\n\t\t\tassert.Equal(t, u.e, ff)\n\t\t})\n\t}\n}\n\nfunc TestFieldClone(t *testing.T) {\n\tf := model1.Fields{\"a\", \"b\", \"c\"}\n\tf1 := f.Clone()\n\n\tassert.True(t, reflect.DeepEqual(f, f1))\n\tassert.NotEqual(t, fmt.Sprintf(\"%p\", f), fmt.Sprintf(\"%p\", f1))\n}\n\nfunc TestRowLabelize(t *testing.T) {\n\tuu := map[string]struct {\n\t\trow  model1.Row\n\t\tcols []int\n\t\te    model1.Row\n\t}{\n\t\t\"empty\": {\n\t\t\trow:  model1.Row{},\n\t\t\tcols: []int{0, 1, 2},\n\t\t\te:    model1.Row{ID: \"\", Fields: model1.Fields{\"\", \"\", \"\"}},\n\t\t},\n\t\t\"no-cols-no-data\": {\n\t\t\trow:  model1.Row{},\n\t\t\tcols: []int{},\n\t\t\te:    model1.Row{ID: \"\", Fields: model1.Fields{}},\n\t\t},\n\t\t\"no-cols-data\": {\n\t\t\trow:  model1.Row{ID: \"fred\", Fields: model1.Fields{\"f1\", \"f2\", \"f3\"}},\n\t\t\tcols: []int{},\n\t\t\te:    model1.Row{ID: \"fred\", Fields: model1.Fields{}},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\trow := u.row.Customize(u.cols)\n\t\t\tassert.Equal(t, u.e, row)\n\t\t})\n\t}\n}\n\nfunc TestRowCustomize(t *testing.T) {\n\tuu := map[string]struct {\n\t\trow  model1.Row\n\t\tcols []int\n\t\te    model1.Row\n\t}{\n\t\t\"empty\": {\n\t\t\trow:  model1.Row{},\n\t\t\tcols: []int{0, 1, 2},\n\t\t\te:    model1.Row{ID: \"\", Fields: model1.Fields{\"\", \"\", \"\"}},\n\t\t},\n\t\t\"no-cols-no-data\": {\n\t\t\trow:  model1.Row{},\n\t\t\tcols: []int{},\n\t\t\te:    model1.Row{ID: \"\", Fields: model1.Fields{}},\n\t\t},\n\t\t\"no-cols-data\": {\n\t\t\trow:  model1.Row{ID: \"fred\", Fields: model1.Fields{\"f1\", \"f2\", \"f3\"}},\n\t\t\tcols: []int{},\n\t\t\te:    model1.Row{ID: \"fred\", Fields: model1.Fields{}},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\trow := u.row.Customize(u.cols)\n\t\t\tassert.Equal(t, u.e, row)\n\t\t})\n\t}\n}\n\nfunc TestRowsDelete(t *testing.T) {\n\tuu := map[string]struct {\n\t\trows model1.Rows\n\t\tid   string\n\t\te    model1.Rows\n\t}{\n\t\t\"first\": {\n\t\t\trows: model1.Rows{\n\t\t\t\t{ID: \"a\", Fields: []string{\"blee\", \"duh\"}},\n\t\t\t\t{ID: \"b\", Fields: []string{\"albert\", \"blee\"}},\n\t\t\t},\n\t\t\tid: \"a\",\n\t\t\te: model1.Rows{\n\t\t\t\t{ID: \"b\", Fields: []string{\"albert\", \"blee\"}},\n\t\t\t},\n\t\t},\n\t\t\"last\": {\n\t\t\trows: model1.Rows{\n\t\t\t\t{ID: \"a\", Fields: []string{\"blee\", \"duh\"}},\n\t\t\t\t{ID: \"b\", Fields: []string{\"albert\", \"blee\"}},\n\t\t\t},\n\t\t\tid: \"b\",\n\t\t\te: model1.Rows{\n\t\t\t\t{ID: \"a\", Fields: []string{\"blee\", \"duh\"}},\n\t\t\t},\n\t\t},\n\t\t\"middle\": {\n\t\t\trows: model1.Rows{\n\t\t\t\t{ID: \"a\", Fields: []string{\"blee\", \"duh\"}},\n\t\t\t\t{ID: \"b\", Fields: []string{\"albert\", \"blee\"}},\n\t\t\t\t{ID: \"c\", Fields: []string{\"fred\", \"zorg\"}},\n\t\t\t},\n\t\t\tid: \"b\",\n\t\t\te: model1.Rows{\n\t\t\t\t{ID: \"a\", Fields: []string{\"blee\", \"duh\"}},\n\t\t\t\t{ID: \"c\", Fields: []string{\"fred\", \"zorg\"}},\n\t\t\t},\n\t\t},\n\t\t\"missing\": {\n\t\t\trows: model1.Rows{\n\t\t\t\t{ID: \"a\", Fields: []string{\"blee\", \"duh\"}},\n\t\t\t\t{ID: \"b\", Fields: []string{\"albert\", \"blee\"}},\n\t\t\t},\n\t\t\tid: \"zorg\",\n\t\t\te: model1.Rows{\n\t\t\t\t{ID: \"a\", Fields: []string{\"blee\", \"duh\"}},\n\t\t\t\t{ID: \"b\", Fields: []string{\"albert\", \"blee\"}},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\trows := u.rows.Delete(u.id)\n\t\t\tassert.Equal(t, u.e, rows)\n\t\t})\n\t}\n}\n\nfunc TestRowsUpsert(t *testing.T) {\n\tuu := map[string]struct {\n\t\trows model1.Rows\n\t\trow  model1.Row\n\t\te    model1.Rows\n\t}{\n\t\t\"add\": {\n\t\t\trows: model1.Rows{\n\t\t\t\t{ID: \"a\", Fields: []string{\"blee\", \"duh\"}},\n\t\t\t\t{ID: \"b\", Fields: []string{\"albert\", \"blee\"}},\n\t\t\t},\n\t\t\trow: model1.Row{ID: \"c\", Fields: []string{\"f1\", \"f2\"}},\n\t\t\te: model1.Rows{\n\t\t\t\t{ID: \"a\", Fields: []string{\"blee\", \"duh\"}},\n\t\t\t\t{ID: \"b\", Fields: []string{\"albert\", \"blee\"}},\n\t\t\t\t{ID: \"c\", Fields: []string{\"f1\", \"f2\"}},\n\t\t\t},\n\t\t},\n\t\t\"update\": {\n\t\t\trows: model1.Rows{\n\t\t\t\t{ID: \"a\", Fields: []string{\"blee\", \"duh\"}},\n\t\t\t\t{ID: \"b\", Fields: []string{\"albert\", \"blee\"}},\n\t\t\t},\n\t\t\trow: model1.Row{ID: \"a\", Fields: []string{\"f1\", \"f2\"}},\n\t\t\te: model1.Rows{\n\t\t\t\t{ID: \"a\", Fields: []string{\"f1\", \"f2\"}},\n\t\t\t\t{ID: \"b\", Fields: []string{\"albert\", \"blee\"}},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\trows := u.rows.Upsert(u.row)\n\t\t\tassert.Equal(t, u.e, rows)\n\t\t})\n\t}\n}\n\nfunc TestRowsSortText(t *testing.T) {\n\tuu := map[string]struct {\n\t\trows     model1.Rows\n\t\tcol      int\n\t\tasc, num bool\n\t\te        model1.Rows\n\t}{\n\t\t\"plainAsc\": {\n\t\t\trows: model1.Rows{\n\t\t\t\t{Fields: []string{\"blee\", \"duh\"}},\n\t\t\t\t{Fields: []string{\"albert\", \"blee\"}},\n\t\t\t},\n\t\t\tcol: 0,\n\t\t\tasc: true,\n\t\t\te: model1.Rows{\n\t\t\t\t{Fields: []string{\"albert\", \"blee\"}},\n\t\t\t\t{Fields: []string{\"blee\", \"duh\"}},\n\t\t\t},\n\t\t},\n\t\t\"plainDesc\": {\n\t\t\trows: model1.Rows{\n\t\t\t\t{Fields: []string{\"blee\", \"duh\"}},\n\t\t\t\t{Fields: []string{\"albert\", \"blee\"}},\n\t\t\t},\n\t\t\tcol: 0,\n\t\t\tasc: false,\n\t\t\te: model1.Rows{\n\t\t\t\t{Fields: []string{\"blee\", \"duh\"}},\n\t\t\t\t{Fields: []string{\"albert\", \"blee\"}},\n\t\t\t},\n\t\t},\n\t\t\"numericAsc\": {\n\t\t\trows: model1.Rows{\n\t\t\t\t{Fields: []string{\"10\", \"duh\"}},\n\t\t\t\t{Fields: []string{\"1\", \"blee\"}},\n\t\t\t},\n\t\t\tcol: 0,\n\t\t\tnum: true,\n\t\t\tasc: true,\n\t\t\te: model1.Rows{\n\t\t\t\t{Fields: []string{\"1\", \"blee\"}},\n\t\t\t\t{Fields: []string{\"10\", \"duh\"}},\n\t\t\t},\n\t\t},\n\t\t\"numericDesc\": {\n\t\t\trows: model1.Rows{\n\t\t\t\t{Fields: []string{\"10\", \"duh\"}},\n\t\t\t\t{Fields: []string{\"1\", \"blee\"}},\n\t\t\t},\n\t\t\tcol: 0,\n\t\t\tnum: true,\n\t\t\tasc: false,\n\t\t\te: model1.Rows{\n\t\t\t\t{Fields: []string{\"10\", \"duh\"}},\n\t\t\t\t{Fields: []string{\"1\", \"blee\"}},\n\t\t\t},\n\t\t},\n\t\t\"composite\": {\n\t\t\trows: model1.Rows{\n\t\t\t\t{Fields: []string{\"blee-duh\", \"duh\"}},\n\t\t\t\t{Fields: []string{\"blee\", \"blee\"}},\n\t\t\t},\n\t\t\tcol: 0,\n\t\t\tasc: true,\n\t\t\te: model1.Rows{\n\t\t\t\t{Fields: []string{\"blee\", \"blee\"}},\n\t\t\t\t{Fields: []string{\"blee-duh\", \"duh\"}},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tu.rows.Sort(u.col, u.asc, u.num, false, false)\n\t\t\tassert.Equal(t, u.e, u.rows)\n\t\t})\n\t}\n}\n\nfunc TestRowsSortDuration(t *testing.T) {\n\tuu := map[string]struct {\n\t\trows model1.Rows\n\t\tcol  int\n\t\tasc  bool\n\t\te    model1.Rows\n\t}{\n\t\t\"fred\": {\n\t\t\trows: model1.Rows{\n\t\t\t\t{Fields: []string{\"2m24s\", \"blee\"}},\n\t\t\t\t{Fields: []string{\"2m12s\", \"duh\"}},\n\t\t\t},\n\t\t\tcol: 0,\n\t\t\tasc: true,\n\t\t\te: model1.Rows{\n\t\t\t\t{Fields: []string{\"2m12s\", \"duh\"}},\n\t\t\t\t{Fields: []string{\"2m24s\", \"blee\"}},\n\t\t\t},\n\t\t},\n\t\t\"years\": {\n\t\t\trows: model1.Rows{\n\t\t\t\t{Fields: []string{testTime().Add(-365 * 24 * time.Hour).String(), \"blee\"}},\n\t\t\t\t{Fields: []string{testTime().String(), \"duh\"}},\n\t\t\t},\n\t\t\tcol: 0,\n\t\t\tasc: true,\n\t\t\te: model1.Rows{\n\t\t\t\t{Fields: []string{testTime().String(), \"duh\"}},\n\t\t\t\t{Fields: []string{testTime().Add(-365 * 24 * time.Hour).String(), \"blee\"}},\n\t\t\t},\n\t\t},\n\t\t\"durationAsc\": {\n\t\t\trows: model1.Rows{\n\t\t\t\t{Fields: []string{testTime().Add(10 * time.Second).String(), \"duh\"}},\n\t\t\t\t{Fields: []string{testTime().String(), \"blee\"}},\n\t\t\t},\n\t\t\tcol: 0,\n\t\t\tasc: true,\n\t\t\te: model1.Rows{\n\t\t\t\t{Fields: []string{testTime().String(), \"blee\"}},\n\t\t\t\t{Fields: []string{testTime().Add(10 * time.Second).String(), \"duh\"}},\n\t\t\t},\n\t\t},\n\t\t\"durationDesc\": {\n\t\t\trows: model1.Rows{\n\t\t\t\t{Fields: []string{testTime().Add(10 * time.Second).String(), \"duh\"}},\n\t\t\t\t{Fields: []string{testTime().String(), \"blee\"}},\n\t\t\t},\n\t\t\tcol: 0,\n\t\t\te: model1.Rows{\n\t\t\t\t{Fields: []string{testTime().Add(10 * time.Second).String(), \"duh\"}},\n\t\t\t\t{Fields: []string{testTime().String(), \"blee\"}},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tu.rows.Sort(u.col, u.asc, false, true, false)\n\t\t\tassert.Equal(t, u.e, u.rows)\n\t\t})\n\t}\n}\n\nfunc TestRowsSortMetrics(t *testing.T) {\n\tuu := map[string]struct {\n\t\trows model1.Rows\n\t\tcol  int\n\t\tasc  bool\n\t\te    model1.Rows\n\t}{\n\t\t\"metricAsc\": {\n\t\t\trows: model1.Rows{\n\t\t\t\t{Fields: []string{\"10m\", \"duh\"}},\n\t\t\t\t{Fields: []string{\"1m\", \"blee\"}},\n\t\t\t},\n\t\t\tcol: 0,\n\t\t\tasc: true,\n\t\t\te: model1.Rows{\n\t\t\t\t{Fields: []string{\"1m\", \"blee\"}},\n\t\t\t\t{Fields: []string{\"10m\", \"duh\"}},\n\t\t\t},\n\t\t},\n\t\t\"metricDesc\": {\n\t\t\trows: model1.Rows{\n\t\t\t\t{Fields: []string{\"10000m\", \"1000Mi\"}},\n\t\t\t\t{Fields: []string{\"1m\", \"50Mi\"}},\n\t\t\t},\n\t\t\tcol: 1,\n\t\t\tasc: false,\n\t\t\te: model1.Rows{\n\t\t\t\t{Fields: []string{\"10000m\", \"1000Mi\"}},\n\t\t\t\t{Fields: []string{\"1m\", \"50Mi\"}},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tu.rows.Sort(u.col, u.asc, true, false, false)\n\t\t\tassert.Equal(t, u.e, u.rows)\n\t\t})\n\t}\n}\n\nfunc TestRowsSortCapacity(t *testing.T) {\n\tuu := map[string]struct {\n\t\trows model1.Rows\n\t\tcol  int\n\t\tasc  bool\n\t\te    model1.Rows\n\t}{\n\t\t\"capacityAsc\": {\n\t\t\trows: model1.Rows{\n\t\t\t\t{Fields: []string{\"10Gi\", \"duh\"}},\n\t\t\t\t{Fields: []string{\"10G\", \"blee\"}},\n\t\t\t},\n\t\t\tcol: 0,\n\t\t\tasc: true,\n\t\t\te: model1.Rows{\n\t\t\t\t{Fields: []string{\"10G\", \"blee\"}},\n\t\t\t\t{Fields: []string{\"10Gi\", \"duh\"}},\n\t\t\t},\n\t\t},\n\t\t\"capacityDesc\": {\n\t\t\trows: model1.Rows{\n\t\t\t\t{Fields: []string{\"10000m\", \"1000Mi\"}},\n\t\t\t\t{Fields: []string{\"1m\", \"50Mi\"}},\n\t\t\t},\n\t\t\tcol: 1,\n\t\t\tasc: false,\n\t\t\te: model1.Rows{\n\t\t\t\t{Fields: []string{\"10000m\", \"1000Mi\"}},\n\t\t\t\t{Fields: []string{\"1m\", \"50Mi\"}},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tu.rows.Sort(u.col, u.asc, false, false, true)\n\t\t\tassert.Equal(t, u.e, u.rows)\n\t\t})\n\t}\n}\n\nfunc TestLess(t *testing.T) {\n\tuu := map[string]struct {\n\t\tisNumber   bool\n\t\tisDuration bool\n\t\tisCapacity bool\n\t\tid1, id2   string\n\t\tv1, v2     string\n\t\te          bool\n\t}{\n\t\t\"years\": {\n\t\t\tisNumber:   false,\n\t\t\tisDuration: true,\n\t\t\tisCapacity: false,\n\t\t\tid1:        \"id1\",\n\t\t\tid2:        \"id2\",\n\t\t\tv1:         \"2y263d\",\n\t\t\tv2:         \"1y179d\",\n\t\t},\n\t\t\"hours\": {\n\t\t\tisNumber:   false,\n\t\t\tisDuration: true,\n\t\t\tisCapacity: false,\n\t\t\tid1:        \"id1\",\n\t\t\tid2:        \"id2\",\n\t\t\tv1:         \"2y263d\",\n\t\t\tv2:         \"19h\",\n\t\t},\n\t\t\"capacity1\": {\n\t\t\tisNumber:   false,\n\t\t\tisDuration: false,\n\t\t\tisCapacity: true,\n\t\t\tid1:        \"id1\",\n\t\t\tid2:        \"id2\",\n\t\t\tv1:         \"1Gi\",\n\t\t\tv2:         \"1G\",\n\t\t\te:          false,\n\t\t},\n\t\t\"capacity2\": {\n\t\t\tisNumber:   false,\n\t\t\tisDuration: false,\n\t\t\tisCapacity: true,\n\t\t\tid1:        \"id1\",\n\t\t\tid2:        \"id2\",\n\t\t\tv1:         \"1Gi\",\n\t\t\tv2:         \"1Ti\",\n\t\t\te:          true,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, model1.Less(u.isNumber, u.isDuration, u.isCapacity, u.id1, u.id2, u.v1, u.v2))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/model1/rows.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model1\n\nimport \"sort\"\n\n// Rows represents a collection of rows.\ntype Rows []Row\n\n// Delete removes an element by id.\nfunc (rr Rows) Delete(id string) Rows {\n\tidx, ok := rr.Find(id)\n\tif !ok {\n\t\treturn rr\n\t}\n\n\tif idx == 0 {\n\t\treturn rr[1:]\n\t}\n\tif idx+1 == len(rr) {\n\t\treturn rr[:len(rr)-1]\n\t}\n\n\treturn append(rr[:idx], rr[idx+1:]...)\n}\n\n// Upsert adds a new item.\nfunc (rr Rows) Upsert(r Row) Rows {\n\tidx, ok := rr.Find(r.ID)\n\tif !ok {\n\t\treturn append(rr, r)\n\t}\n\trr[idx] = r\n\n\treturn rr\n}\n\n// Find locates a row by id. Returns false is not found.\nfunc (rr Rows) Find(id string) (int, bool) {\n\tfor i, r := range rr {\n\t\tif r.ID == id {\n\t\t\treturn i, true\n\t\t}\n\t}\n\n\treturn 0, false\n}\n\n// Sort rows based on column index and order.\nfunc (rr Rows) Sort(col int, asc, isNum, isDur, isCapacity bool) {\n\tt := RowSorter{\n\t\tRows:       rr,\n\t\tIndex:      col,\n\t\tIsNumber:   isNum,\n\t\tIsDuration: isDur,\n\t\tIsCapacity: isCapacity,\n\t\tAsc:        asc,\n\t}\n\tsort.Sort(t)\n}\n"
  },
  {
    "path": "internal/model1/table_data.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model1\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/sahilm/fuzzy\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/util/sets\"\n)\n\n// SortFn represent a function that can sort columnar data.\ntype SortFn func(rows Rows, sortCol SortColumn)\n\n// SortColumn represents a sortable column.\ntype SortColumn struct {\n\tName string\n\tASC  bool\n}\n\n// IsSet checks if the sort column is set.\nfunc (s SortColumn) IsSet() bool {\n\treturn s.Name != \"\"\n}\n\nconst spacer = \" \"\n\ntype FilterOpts struct {\n\tToast  bool\n\tFilter string\n\tInvert bool\n}\n\n// TableData tracks a K8s resource for tabular display.\ntype TableData struct {\n\theader    Header\n\trowEvents *RowEvents\n\tnamespace string\n\tgvr       *client.GVR\n\tmx        sync.RWMutex\n}\n\n// NewTableData returns a new table.\nfunc NewTableData(gvr *client.GVR) *TableData {\n\treturn &TableData{\n\t\tgvr:       gvr,\n\t\trowEvents: NewRowEvents(10),\n\t}\n}\n\nfunc NewTableDataFull(gvr *client.GVR, ns string, h Header, re *RowEvents) *TableData {\n\tt := NewTableDataWithRows(gvr, h, re)\n\tt.namespace = ns\n\n\treturn t\n}\n\nfunc NewTableDataWithRows(gvr *client.GVR, h Header, re *RowEvents) *TableData {\n\tt := NewTableData(gvr)\n\tt.header, t.rowEvents = h, re\n\n\treturn t\n}\n\nfunc NewTableDataFromTable(td *TableData) *TableData {\n\tt := NewTableData(td.gvr)\n\tt.header = td.header\n\tt.rowEvents = td.rowEvents\n\tt.namespace = td.namespace\n\n\treturn t\n}\n\nfunc (t *TableData) AddRow(re RowEvent) {\n\tt.rowEvents.Add(re)\n}\n\nfunc (t *TableData) SetRow(idx int, re RowEvent) {\n\tt.rowEvents.Set(idx, re)\n}\n\nfunc (t *TableData) FindRow(id string) (RowEvent, bool) {\n\treturn t.rowEvents.Get(id)\n}\n\nfunc (t *TableData) RowAt(idx int) (RowEvent, bool) {\n\treturn t.rowEvents.At(idx)\n}\n\nfunc (t *TableData) RowsRange(f ReRangeFn) {\n\tt.rowEvents.Range(f)\n}\n\nfunc (t *TableData) Sort(sc SortColumn) {\n\tcol, idx := t.HeadCol(sc.Name, false)\n\tif idx < 0 {\n\t\treturn\n\t}\n\tt.rowEvents.Sort(\n\t\tt.GetNamespace(),\n\t\tidx,\n\t\tcol.Time,\n\t\tcol.MX,\n\t\tcol.Capacity,\n\t\tsc.ASC,\n\t)\n}\n\nfunc (t *TableData) Header() Header {\n\treturn t.header\n}\n\n// HeaderCount returns the number of header cols.\nfunc (t *TableData) HeaderCount() int {\n\tt.mx.RLock()\n\tdefer t.mx.RUnlock()\n\n\treturn len(t.header)\n}\n\nfunc (t *TableData) HeadCol(n string, w bool) (header HeaderColumn, idx int) {\n\tidx, ok := t.header.IndexOf(n, w)\n\tif !ok {\n\t\treturn HeaderColumn{}, -1\n\t}\n\n\treturn t.header[idx], idx\n}\n\nfunc (t *TableData) Filter(f FilterOpts) *TableData {\n\ttd := NewTableDataFromTable(t)\n\n\tif f.Toast {\n\t\ttd.rowEvents = t.filterToast()\n\t}\n\tif f.Filter == \"\" || internal.IsLabelSelector(f.Filter) {\n\t\treturn td\n\t}\n\tif f, ok := internal.IsFuzzySelector(f.Filter); ok {\n\t\ttd.rowEvents = t.fuzzyFilter(f)\n\t\treturn td\n\t}\n\trr, err := t.rxFilter(f.Filter, internal.IsInverseSelector(f.Filter))\n\tif err == nil {\n\t\ttd.rowEvents = rr\n\t} else {\n\t\tslog.Error(\"RX filter failed\", slogs.Error, err)\n\t}\n\n\treturn td\n}\n\nfunc (t *TableData) rxFilter(q string, inverse bool) (*RowEvents, error) {\n\tif strings.Contains(q, \" \") {\n\t\treturn t.rowEvents, nil\n\t}\n\n\tif inverse {\n\t\tq = q[1:]\n\t}\n\trx, err := regexp.Compile(`(?i)(` + q + `)`)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"invalid rx filter %q: %w\", q, err)\n\t}\n\n\tvidx := t.header.FilterColIndices(t.namespace, true)\n\trr := NewRowEvents(t.RowCount() / 2)\n\tt.rowEvents.Range(func(_ int, re RowEvent) bool {\n\t\tff := make([]string, 0, len(re.Row.Fields))\n\t\tfor idx, r := range re.Row.Fields {\n\t\t\tif !vidx.Has(idx) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tff = append(ff, r)\n\t\t}\n\t\tmatch := rx.MatchString(strings.Join(ff, spacer))\n\t\tif (inverse && !match) || (!inverse && match) {\n\t\t\trr.Add(re)\n\t\t}\n\n\t\treturn true\n\t})\n\n\treturn rr, nil\n}\n\nfunc (t *TableData) fuzzyFilter(q string) *RowEvents {\n\tq = strings.TrimSpace(q)\n\tss := make([]string, 0, t.RowCount()/2)\n\tt.rowEvents.Range(func(_ int, re RowEvent) bool {\n\t\tss = append(ss, re.Row.ID)\n\t\treturn true\n\t})\n\n\tmm := fuzzy.Find(q, ss)\n\trr := NewRowEvents(t.RowCount() / 2)\n\tfor _, m := range mm {\n\t\tif re, ok := t.rowEvents.At(m.Index); !ok {\n\t\t\tslog.Error(\"Unable to find event for index in fuzzfilter\", slogs.Index, m.Index)\n\t\t} else {\n\t\t\trr.Add(re)\n\t\t}\n\t}\n\n\treturn rr\n}\n\nfunc (t *TableData) filterToast() *RowEvents {\n\trr := NewRowEvents(10)\n\tidx, ok := t.header.IndexOf(\"VALID\", true)\n\tif !ok {\n\t\treturn rr\n\t}\n\tt.rowEvents.Range(func(_ int, re RowEvent) bool {\n\t\tif re.Row.Fields[idx] != \"\" {\n\t\t\trr.Add(re)\n\t\t}\n\t\treturn true\n\t})\n\n\treturn rr\n}\n\nfunc (t *TableData) GetNamespace() string {\n\tt.mx.RLock()\n\tdefer t.mx.RUnlock()\n\n\treturn t.namespace\n}\n\nfunc (t *TableData) Reset(ns string) {\n\tt.mx.Lock()\n\tt.namespace = ns\n\tt.mx.Unlock()\n\n\tt.Clear()\n}\n\nfunc (t *TableData) Render(_ context.Context, r Renderer, oo []runtime.Object) error {\n\tvar rows Rows\n\tif len(oo) > 0 {\n\t\tif r.IsGeneric() {\n\t\t\ttable, ok := oo[0].(*metav1.Table)\n\t\t\tif !ok {\n\t\t\t\treturn fmt.Errorf(\"expecting a meta table but got %T\", oo[0])\n\t\t\t}\n\t\t\trows = make(Rows, len(table.Rows))\n\t\t\tif err := GenericHydrate(t.namespace, table, rows, r); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\trows = make(Rows, len(oo))\n\t\t\tif err := Hydrate(t.namespace, oo, rows, r); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tt.Update(rows)\n\tt.SetHeader(t.namespace, r.Header(t.namespace))\n\tif t.HeaderCount() == 0 {\n\t\treturn fmt.Errorf(\"no data found for resource %s\", t.gvr)\n\t}\n\n\treturn nil\n}\n\n// Empty checks if there are no entries.\nfunc (t *TableData) Empty() bool {\n\tt.mx.RLock()\n\tdefer t.mx.RUnlock()\n\n\treturn t.rowEvents.Empty()\n}\n\nfunc (t *TableData) SetRowEvents(re *RowEvents) {\n\tt.rowEvents = re\n}\n\nfunc (t *TableData) GetRowEvents() *RowEvents {\n\treturn t.rowEvents\n}\n\n// RowCount returns the number of rows.\nfunc (t *TableData) RowCount() int {\n\tt.mx.RLock()\n\tdefer t.mx.RUnlock()\n\n\treturn t.rowEvents.Len()\n}\n\n// IndexOfHeader return the index of the header.\nfunc (t *TableData) IndexOfHeader(h string) (int, bool) {\n\treturn t.header.IndexOf(h, false)\n}\n\n// Labelize prints out specific label columns.\nfunc (t *TableData) Labelize(labels []string) *TableData {\n\tidx, ok := t.header.IndexOf(\"LABELS\", true)\n\tif !ok {\n\t\treturn t\n\t}\n\tcols := []int{0, 1}\n\tif client.IsNamespaced(t.namespace) {\n\t\tcols = cols[1:]\n\t}\n\tdata := TableData{\n\t\tnamespace: t.namespace,\n\t\theader:    t.header.Labelize(cols, idx, t.rowEvents),\n\t}\n\tdata.rowEvents = t.rowEvents.Labelize(cols, idx, labels)\n\n\treturn &data\n}\n\n// ComputeSortCol computes the best matched sort column.\nfunc (t *TableData) ComputeSortCol(vs *config.ViewSetting, sc SortColumn, manual bool) SortColumn {\n\tif vs.IsBlank() {\n\t\tif sc.Name != \"\" {\n\t\t\treturn sc\n\t\t}\n\t\tif psc, err := t.sortCol(vs); err == nil {\n\t\t\treturn psc\n\t\t}\n\t\treturn sc\n\t}\n\tif manual && sc.IsSet() {\n\t\treturn sc\n\t}\n\tif s, asc, err := vs.SortCol(); err == nil {\n\t\treturn SortColumn{Name: s, ASC: asc}\n\t}\n\n\treturn sc\n}\n\nfunc (t *TableData) sortCol(vs *config.ViewSetting) (SortColumn, error) {\n\tvar psc SortColumn\n\n\tif t.HeaderCount() == 0 {\n\t\treturn psc, errors.New(\"no header found\")\n\t}\n\tname, order, _ := vs.SortCol()\n\tif _, ok := t.header.IndexOf(name, false); ok {\n\t\tpsc.Name, psc.ASC = name, order\n\t\treturn psc, nil\n\t}\n\tif client.IsAllNamespaces(t.GetNamespace()) {\n\t\tif _, ok := t.header.IndexOf(\"NAMESPACE\", false); ok {\n\t\t\tpsc.Name = \"NAMESPACE\"\n\t\t} else if _, ok := t.header.IndexOf(\"NAME\", false); ok {\n\t\t\tpsc.Name = \"NAME\"\n\t\t}\n\t} else {\n\t\tif _, ok := t.header.IndexOf(\"NAME\", false); ok {\n\t\t\tpsc.Name = \"NAME\"\n\t\t} else {\n\t\t\tpsc.Name = t.header[0].Name\n\t\t}\n\t}\n\tpsc.ASC = true\n\n\treturn psc, nil\n}\n\n// Clear clears out the entire table.\nfunc (t *TableData) Clear() {\n\tt.mx.Lock()\n\tdefer t.mx.Unlock()\n\n\tt.header = t.header.Clear()\n\tt.rowEvents.Clear()\n}\n\n// Clone returns a copy of the table.\nfunc (t *TableData) Clone() *TableData {\n\tt.mx.RLock()\n\tdefer t.mx.RUnlock()\n\n\treturn &TableData{\n\t\theader:    t.header.Clone(),\n\t\trowEvents: t.rowEvents.Clone(),\n\t\tnamespace: t.namespace,\n\t\tgvr:       t.gvr,\n\t}\n}\n\nfunc (t *TableData) ColumnNames(w bool) []string {\n\tt.mx.RLock()\n\tdefer t.mx.RUnlock()\n\n\treturn t.header.ColumnNames(w)\n}\n\n// GetHeader returns table header.\nfunc (t *TableData) GetHeader() Header {\n\tt.mx.RLock()\n\tdefer t.mx.RUnlock()\n\n\treturn t.header\n}\n\n// SetHeader sets table header.\nfunc (t *TableData) SetHeader(ns string, h Header) {\n\tt.mx.Lock()\n\tdefer t.mx.Unlock()\n\n\tt.namespace, t.header = ns, h\n}\n\n// Update computes row deltas and update the table data.\nfunc (t *TableData) Update(rows Rows) {\n\tempty := t.Empty()\n\tkk := sets.New[string]()\n\tvar blankDelta DeltaRow\n\tt.mx.Lock()\n\tfor _, row := range rows {\n\t\tkk.Insert(row.ID)\n\t\tif empty {\n\t\t\tt.rowEvents.Add(NewRowEvent(EventAdd, row))\n\t\t\tcontinue\n\t\t}\n\t\tif index, ok := t.rowEvents.FindIndex(row.ID); ok {\n\t\t\tev, ok := t.rowEvents.At(index)\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tdelta := NewDeltaRow(ev.Row, row, t.header)\n\t\t\tif delta.IsBlank() {\n\t\t\t\tev.Kind, ev.Deltas, ev.Row = EventUnchanged, blankDelta, row\n\t\t\t\tt.rowEvents.Set(index, ev)\n\t\t\t} else {\n\t\t\t\tt.rowEvents.Set(index, NewRowEventWithDeltas(row, delta))\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tt.rowEvents.Add(NewRowEvent(EventAdd, row))\n\t}\n\tt.mx.Unlock()\n\n\tif !empty {\n\t\tt.Delete(kk)\n\t}\n}\n\n// Delete removes items in cache that are no longer valid.\nfunc (t *TableData) Delete(newKeys sets.Set[string]) {\n\tt.mx.Lock()\n\tdefer t.mx.Unlock()\n\n\tvictims := sets.New[string]()\n\tt.rowEvents.Range(func(_ int, e RowEvent) bool {\n\t\tif newKeys.Has(e.Row.ID) {\n\t\t\tdelete(newKeys, e.Row.ID)\n\t\t} else {\n\t\t\tvictims.Insert(e.Row.ID)\n\t\t}\n\t\treturn true\n\t})\n\n\tfor _, id := range victims.UnsortedList() {\n\t\tif err := t.rowEvents.Delete(id); err != nil {\n\t\t\tslog.Error(\"Table delete failed\",\n\t\t\t\tslogs.Error, err,\n\t\t\t\tslogs.Message, id,\n\t\t\t)\n\t\t}\n\t}\n}\n\n// Diff checks if two tables are equal.\nfunc (t *TableData) Diff(t2 *TableData) bool {\n\tif t2 == nil || t.namespace != t2.namespace || t.header.Diff(t2.header) {\n\t\treturn true\n\t}\n\tidx, ok := t.header.IndexOf(\"AGE\", true)\n\tif !ok {\n\t\tidx = -1\n\t}\n\treturn t.rowEvents.Diff(t2.rowEvents, idx)\n}\n"
  },
  {
    "path": "internal/model1/table_data_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model1\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"k8s.io/apimachinery/pkg/util/sets\"\n)\n\nfunc init() {\n\tslog.SetDefault(slog.New(slog.DiscardHandler))\n}\n\nfunc TestTableDataComputeSortCol(t *testing.T) {\n\tuu := map[string]struct {\n\t\tt1           *TableData\n\t\tvs           config.ViewSetting\n\t\tsc           SortColumn\n\t\twide, manual bool\n\t\te            SortColumn\n\t}{\n\t\t\"same\": {\n\t\t\tt1: NewTableDataWithRows(\n\t\t\t\tclient.NewGVR(\"test\"),\n\t\t\t\tHeader{\n\t\t\t\t\tHeaderColumn{Name: \"A\"},\n\t\t\t\t\tHeaderColumn{Name: \"B\"},\n\t\t\t\t\tHeaderColumn{Name: \"C\"},\n\t\t\t\t},\n\t\t\t\tNewRowEventsWithEvts(\n\t\t\t\t\tRowEvent{Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t\t),\n\t\t\t),\n\t\t\tvs: config.ViewSetting{Columns: []string{\"A\", \"B\", \"C\"}, SortColumn: \"A:asc\"},\n\t\t\te:  SortColumn{Name: \"A\", ASC: true},\n\t\t},\n\t\t\"wide-col\": {\n\t\t\tt1: NewTableDataWithRows(\n\t\t\t\tclient.NewGVR(\"test\"),\n\t\t\t\tHeader{\n\t\t\t\t\tHeaderColumn{Name: \"A\"},\n\t\t\t\t\tHeaderColumn{Name: \"B\", Attrs: Attrs{Wide: true}},\n\t\t\t\t\tHeaderColumn{Name: \"C\"},\n\t\t\t\t},\n\t\t\t\tNewRowEventsWithEvts(\n\t\t\t\t\tRowEvent{Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t\t),\n\t\t\t),\n\t\t\tvs: config.ViewSetting{Columns: []string{\"A\", \"B\", \"C\"}, SortColumn: \"B:desc\"},\n\t\t\te:  SortColumn{Name: \"B\"},\n\t\t},\n\n\t\t\"wide\": {\n\t\t\tt1: NewTableDataWithRows(\n\t\t\t\tclient.NewGVR(\"test\"),\n\t\t\t\tHeader{\n\t\t\t\t\tHeaderColumn{Name: \"A\"},\n\t\t\t\t\tHeaderColumn{Name: \"B\", Attrs: Attrs{Wide: true}},\n\t\t\t\t\tHeaderColumn{Name: \"C\"},\n\t\t\t\t},\n\t\t\t\tNewRowEventsWithEvts(\n\t\t\t\t\tRowEvent{Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t\t),\n\t\t\t),\n\t\t\twide: true,\n\t\t\tvs:   config.ViewSetting{Columns: []string{\"A\", \"C\"}, SortColumn: \"\"},\n\t\t\te:    SortColumn{Name: \"\"},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tsc := u.t1.ComputeSortCol(&u.vs, u.sc, u.manual)\n\t\t\tassert.Equal(t, u.e, sc)\n\t\t})\n\t}\n}\n\nfunc TestTableDataDiff(t *testing.T) {\n\tuu := map[string]struct {\n\t\tt1, t2 *TableData\n\t\te      bool\n\t}{\n\t\t\"empty\": {\n\t\t\tt1: NewTableDataWithRows(\n\t\t\t\tclient.NewGVR(\"test\"),\n\t\t\t\tHeader{\n\t\t\t\t\tHeaderColumn{Name: \"A\"},\n\t\t\t\t\tHeaderColumn{Name: \"B\"},\n\t\t\t\t\tHeaderColumn{Name: \"C\"},\n\t\t\t\t},\n\t\t\t\tNewRowEventsWithEvts(\n\t\t\t\t\tRowEvent{Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t\t),\n\t\t\t),\n\t\t\te: true,\n\t\t},\n\t\t\"same\": {\n\t\t\tt1: NewTableDataWithRows(\n\t\t\t\tclient.NewGVR(\"test\"),\n\t\t\t\tHeader{\n\t\t\t\t\tHeaderColumn{Name: \"A\"},\n\t\t\t\t\tHeaderColumn{Name: \"B\"},\n\t\t\t\t\tHeaderColumn{Name: \"C\"},\n\t\t\t\t},\n\t\t\t\tNewRowEventsWithEvts(\n\t\t\t\t\tRowEvent{Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t\t),\n\t\t\t),\n\t\t\tt2: NewTableDataWithRows(\n\t\t\t\tclient.NewGVR(\"test\"),\n\t\t\t\tHeader{\n\t\t\t\t\tHeaderColumn{Name: \"A\"},\n\t\t\t\t\tHeaderColumn{Name: \"B\"},\n\t\t\t\t\tHeaderColumn{Name: \"C\"},\n\t\t\t\t},\n\t\t\t\tNewRowEventsWithEvts(\n\t\t\t\t\tRowEvent{Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t\t),\n\t\t\t),\n\t\t},\n\t\t\"ns-diff\": {\n\t\t\tt1: NewTableDataFull(\n\t\t\t\tclient.NewGVR(\"test\"),\n\t\t\t\t\"ns1\",\n\t\t\t\tHeader{\n\t\t\t\t\tHeaderColumn{Name: \"A\"},\n\t\t\t\t\tHeaderColumn{Name: \"B\"},\n\t\t\t\t\tHeaderColumn{Name: \"C\"},\n\t\t\t\t},\n\t\t\t\tNewRowEventsWithEvts(\n\t\t\t\t\tRowEvent{Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t\t),\n\t\t\t),\n\t\t\tt2: NewTableDataFull(\n\t\t\t\tclient.NewGVR(\"test\"),\n\t\t\t\t\"ns-2\",\n\t\t\t\tHeader{\n\t\t\t\t\tHeaderColumn{Name: \"A\"},\n\t\t\t\t\tHeaderColumn{Name: \"B\"},\n\t\t\t\t\tHeaderColumn{Name: \"C\"},\n\t\t\t\t},\n\t\t\t\tNewRowEventsWithEvts(\n\t\t\t\t\tRowEvent{Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t\t),\n\t\t\t),\n\t\t\te: true,\n\t\t},\n\t\t\"header-diff\": {\n\t\t\tt1: NewTableDataWithRows(\n\t\t\t\tclient.NewGVR(\"test\"),\n\t\t\t\tHeader{\n\t\t\t\t\tHeaderColumn{Name: \"A\"},\n\t\t\t\t\tHeaderColumn{Name: \"D\"},\n\t\t\t\t\tHeaderColumn{Name: \"C\"},\n\t\t\t\t},\n\t\t\t\tNewRowEventsWithEvts(\n\t\t\t\t\tRowEvent{Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t\t),\n\t\t\t),\n\t\t\tt2: NewTableDataWithRows(\n\t\t\t\tclient.NewGVR(\"test\"),\n\t\t\t\tHeader{\n\t\t\t\t\tHeaderColumn{Name: \"A\"},\n\t\t\t\t\tHeaderColumn{Name: \"B\"},\n\t\t\t\t\tHeaderColumn{Name: \"C\"},\n\t\t\t\t},\n\t\t\t\tNewRowEventsWithEvts(\n\t\t\t\t\tRowEvent{Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t\t),\n\t\t\t),\n\t\t\te: true,\n\t\t},\n\t\t\"row-diff\": {\n\t\t\tt1: NewTableDataWithRows(\n\t\t\t\tclient.NewGVR(\"test\"),\n\t\t\t\tHeader{\n\t\t\t\t\tHeaderColumn{Name: \"A\"},\n\t\t\t\t\tHeaderColumn{Name: \"B\"},\n\t\t\t\t\tHeaderColumn{Name: \"C\"},\n\t\t\t\t},\n\t\t\t\tNewRowEventsWithEvts(\n\t\t\t\t\tRowEvent{Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t\t),\n\t\t\t),\n\t\t\tt2: NewTableDataWithRows(\n\t\t\t\tclient.NewGVR(\"test\"),\n\t\t\t\tHeader{\n\t\t\t\t\tHeaderColumn{Name: \"A\"},\n\t\t\t\t\tHeaderColumn{Name: \"B\"},\n\t\t\t\t\tHeaderColumn{Name: \"C\"},\n\t\t\t\t},\n\t\t\t\tNewRowEventsWithEvts(\n\t\t\t\t\tRowEvent{Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\t\tRowEvent{Row: Row{ID: \"C\", Fields: Fields{\"100\", \"2\", \"3\"}}},\n\t\t\t\t),\n\t\t\t),\n\t\t\te: true,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.t1.Diff(u.t2))\n\t\t})\n\t}\n}\n\nfunc TestTableDataUpdate(t *testing.T) {\n\tuu := map[string]struct {\n\t\tre, e *RowEvents\n\t\trr    Rows\n\t}{\n\t\t\"no-change\": {\n\t\t\tre: NewRowEventsWithEvts(\n\t\t\t\tRowEvent{Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\trr: Rows{\n\t\t\t\tRow{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}},\n\t\t\t\tRow{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}},\n\t\t\t\tRow{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}},\n\t\t\t},\n\t\t\te: NewRowEventsWithEvts(\n\t\t\t\tRowEvent{Kind: EventUnchanged, Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Kind: EventUnchanged, Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Kind: EventUnchanged, Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t},\n\t\t\"add\": {\n\t\t\tre: NewRowEventsWithEvts(\n\t\t\t\tRowEvent{Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\trr: Rows{\n\t\t\t\tRow{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}},\n\t\t\t\tRow{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}},\n\t\t\t\tRow{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}},\n\t\t\t\tRow{ID: \"D\", Fields: Fields{\"10\", \"2\", \"3\"}},\n\t\t\t},\n\t\t\te: NewRowEventsWithEvts(\n\t\t\t\tRowEvent{Kind: EventUnchanged, Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Kind: EventUnchanged, Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Kind: EventUnchanged, Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Kind: EventAdd, Row: Row{ID: \"D\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t},\n\t\t\"delete\": {\n\t\t\tre: NewRowEventsWithEvts(\n\t\t\t\tRowEvent{Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\trr: Rows{\n\t\t\t\tRow{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}},\n\t\t\t\tRow{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}},\n\t\t\t},\n\t\t\te: NewRowEventsWithEvts(\n\t\t\t\tRowEvent{Kind: EventUnchanged, Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Kind: EventUnchanged, Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t},\n\t\t\"update\": {\n\t\t\tre: NewRowEventsWithEvts(\n\t\t\t\tRowEvent{Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\trr: Rows{\n\t\t\t\tRow{ID: \"A\", Fields: Fields{\"10\", \"2\", \"3\"}},\n\t\t\t\tRow{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}},\n\t\t\t\tRow{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}},\n\t\t\t},\n\t\t\te: NewRowEventsWithEvts(\n\t\t\t\tRowEvent{\n\t\t\t\t\tKind:   EventUpdate,\n\t\t\t\t\tRow:    Row{ID: \"A\", Fields: Fields{\"10\", \"2\", \"3\"}},\n\t\t\t\t\tDeltas: DeltaRow{\"1\", \"\", \"\"},\n\t\t\t\t},\n\t\t\t\tRowEvent{Kind: EventUnchanged, Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Kind: EventUnchanged, Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t},\n\t}\n\n\tvar table TableData\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\ttable.SetRowEvents(u.re)\n\t\t\ttable.Update(u.rr)\n\t\t\tassert.Equal(t, u.e, table.GetRowEvents())\n\t\t})\n\t}\n}\n\nfunc TestTableDataDelete(t *testing.T) {\n\tuu := map[string]struct {\n\t\tre, e *RowEvents\n\t\tkk    sets.Set[string]\n\t}{\n\t\t\"ordered\": {\n\t\t\tre: NewRowEventsWithEvts(\n\t\t\t\tRowEvent{Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tkk: sets.New[string](\"A\", \"C\"),\n\t\t\te: NewRowEventsWithEvts(\n\t\t\t\tRowEvent{Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t},\n\t\t\"unordered\": {\n\t\t\tre: NewRowEventsWithEvts(\n\t\t\t\tRowEvent{Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Row: Row{ID: \"B\", Fields: Fields{\"0\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Row: Row{ID: \"D\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t\tkk: sets.New[string](\"C\", \"A\"),\n\t\t\te: NewRowEventsWithEvts(\n\t\t\t\tRowEvent{Row: Row{ID: \"A\", Fields: Fields{\"1\", \"2\", \"3\"}}},\n\t\t\t\tRowEvent{Row: Row{ID: \"C\", Fields: Fields{\"10\", \"2\", \"3\"}}},\n\t\t\t),\n\t\t},\n\t}\n\n\tvar table TableData\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\ttable.SetRowEvents(u.re)\n\t\t\ttable.Delete(u.kk)\n\t\t\tassert.Equal(t, u.e, table.GetRowEvents())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/model1/test_helper_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model1_test\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\nfunc testTime() time.Time {\n\tt, err := time.Parse(time.RFC3339, \"2018-12-14T10:36:43.326972-07:00\")\n\tif err != nil {\n\t\tfmt.Println(\"TestTime Failed\", err)\n\t}\n\treturn t\n}\n"
  },
  {
    "path": "internal/model1/types.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage model1\n\nimport (\n\t\"context\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/tcell/v2\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nconst (\n\tNAValue = \"na\"\n\n\t// EventUnchanged notifies listener resource has not changed.\n\tEventUnchanged ResEvent = 1 << iota\n\n\t// EventAdd notifies listener of a resource was added.\n\tEventAdd\n\n\t// EventUpdate notifies listener of a resource updated.\n\tEventUpdate\n\n\t// EventDelete  notifies listener of a resource was deleted.\n\tEventDelete\n\n\t// EventClear the stack was reset.\n\tEventClear\n)\n\n// DecoratorFunc decorates a string.\ntype DecoratorFunc func(string) string\n\n// ColorerFunc represents a resource row colorer.\ntype ColorerFunc func(ns string, h Header, re *RowEvent) tcell.Color\n\n// Renderer represents a resource renderer.\ntype Renderer interface {\n\t// IsGeneric identifies a generic handler.\n\tIsGeneric() bool\n\n\t// Render converts raw resources to tabular data.\n\tRender(o any, ns string, row *Row) error\n\n\t// Header returns the resource header.\n\tHeader(ns string) Header\n\n\t// ColorerFunc returns a row colorer function.\n\tColorerFunc() ColorerFunc\n\n\t// SetViewSetting sets custom view settings if any.\n\tSetViewSetting(vs *config.ViewSetting)\n\n\t// Healthy checks if the resource is healthy.\n\tHealthy(ctx context.Context, o any) error\n}\n\n// Generic represents a generic resource.\ntype Generic interface {\n\t// SetTable sets up the resource tabular definition.\n\tSetTable(ns string, table *metav1.Table)\n\n\t// Header returns a resource header.\n\tHeader(ns string) Header\n\n\t// Render renders the resource.\n\tRender(o any, ns string, row *Row) error\n}\n"
  },
  {
    "path": "internal/perf/benchmark.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage perf\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/rakyll/hey/requester\"\n)\n\nconst (\n\t// BOZO!! Revisit bench and when we should timeout.\n\tbenchTimeout = 2 * time.Minute\n\tbenchFmat    = \"%s_%s_%d.txt\"\n\tk9sUA        = \"k9s/\"\n)\n\n// Benchmark puts a workload under load.\ntype Benchmark struct {\n\tcanceled bool\n\tconfig   *config.BenchConfig\n\tworker   *requester.Work\n\tcancelFn context.CancelFunc\n\tmx       sync.RWMutex\n}\n\n// NewBenchmark returns a new benchmark.\nfunc NewBenchmark(base, version string, cfg *config.BenchConfig) (*Benchmark, error) {\n\tb := Benchmark{config: cfg}\n\tif err := b.init(base, version); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &b, nil\n}\n\nfunc (b *Benchmark) init(base, version string) error {\n\tvar ctx context.Context\n\tctx, b.cancelFn = context.WithTimeout(context.Background(), benchTimeout)\n\treq, err := http.NewRequestWithContext(ctx, b.config.HTTP.Method, base, http.NoBody)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif b.config.Auth.User != \"\" || b.config.Auth.Password != \"\" {\n\t\treq.SetBasicAuth(b.config.Auth.User, b.config.Auth.Password)\n\t}\n\treq.Header = b.config.HTTP.Headers\n\tslog.Debug(\"Benchmarking Request\", slogs.URL, req.URL.String())\n\n\tua := req.UserAgent()\n\tif ua == \"\" {\n\t\tua = k9sUA\n\t} else {\n\t\tua += \" \" + k9sUA\n\t}\n\tua += version\n\tif req.Header == nil {\n\t\treq.Header = make(http.Header)\n\t}\n\treq.Header.Set(\"User-Agent\", ua)\n\n\tslog.Debug(fmt.Sprintf(\"Using bench config N:%d--C:%d\", b.config.N, b.config.C))\n\tb.worker = &requester.Work{\n\t\tRequest:     req,\n\t\tRequestBody: []byte(b.config.HTTP.Body),\n\t\tN:           b.config.N,\n\t\tC:           b.config.C,\n\t\tH2:          b.config.HTTP.HTTP2,\n\t}\n\n\treturn nil\n}\n\n// Cancel kills the benchmark in progress.\nfunc (b *Benchmark) Cancel() {\n\tif b == nil {\n\t\treturn\n\t}\n\n\tb.mx.Lock()\n\tdefer b.mx.Unlock()\n\tb.canceled = true\n\tif b.cancelFn != nil {\n\t\tb.cancelFn()\n\t\tb.cancelFn = nil\n\t}\n}\n\n// Canceled checks if the benchmark was canceled.\nfunc (b *Benchmark) Canceled() bool {\n\treturn b.canceled\n}\n\n// Run starts a benchmark.\nfunc (b *Benchmark) Run(cluster, ct string, done func()) {\n\tslog.Debug(\"Running benchmark\",\n\t\tslogs.Cluster, cluster,\n\t\tslogs.Context, ct,\n\t)\n\tbuff := new(bytes.Buffer)\n\tb.worker.Writer = buff\n\t// this call will block until the benchmark is complete or times out.\n\tb.worker.Run()\n\tb.worker.Stop()\n\tif buff.Len() > 0 {\n\t\tif err := b.save(cluster, ct, buff); err != nil {\n\t\t\tslog.Error(\"Saving Benchmark\", slogs.Error, err)\n\t\t}\n\t}\n\tdone()\n}\n\nfunc (b *Benchmark) save(cluster, ct string, r io.Reader) error {\n\tns, n := client.Namespaced(b.config.Name)\n\tn = strings.ReplaceAll(n, \"|\", \"_\")\n\tn = strings.ReplaceAll(n, \":\", \"_\")\n\tdir, err := config.EnsureBenchmarksDir(cluster, ct)\n\tif err != nil {\n\t\treturn err\n\t}\n\tbf := filepath.Join(dir, fmt.Sprintf(benchFmat, ns, n, time.Now().UnixNano()))\n\tif e := data.EnsureDirPath(bf, data.DefaultDirMod); e != nil {\n\t\treturn e\n\t}\n\n\tf, err := os.Create(bf)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() {\n\t\tif e := f.Close(); e != nil {\n\t\t\tslog.Error(\"Benchmark file close failed\",\n\t\t\t\tslogs.Error, e,\n\t\t\t\tslogs.Path, bf,\n\t\t\t)\n\t\t}\n\t}()\n\tif _, err = io.Copy(f, r); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/pool.go",
    "content": "package internal\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"sync\"\n\n\t\"github.com/derailed/k9s/internal/slogs\"\n)\n\nconst DefaultPoolSize = 10\n\ntype jobFn func(ctx context.Context) error\n\ntype WorkerPool struct {\n\tsemC     chan struct{}\n\terrC     chan error\n\tctx      context.Context\n\tcancelFn context.CancelFunc\n\tmx       sync.RWMutex\n\twg       sync.WaitGroup\n\twge      sync.WaitGroup\n\terrs     []error\n}\n\nfunc NewWorkerPool(ctx context.Context, size int) *WorkerPool {\n\t_, cancelFn := context.WithCancel(ctx)\n\n\tp := WorkerPool{\n\t\tsemC:     make(chan struct{}, size),\n\t\terrC:     make(chan error, 1),\n\t\tcancelFn: cancelFn,\n\t\tctx:      ctx,\n\t}\n\n\tp.wge.Add(1)\n\tgo func(wg *sync.WaitGroup) {\n\t\tdefer wg.Done()\n\t\tfor err := range p.errC {\n\t\t\tif err != nil {\n\t\t\t\tp.mx.Lock()\n\t\t\t\tp.errs = append(p.errs, err)\n\t\t\t\tp.mx.Unlock()\n\t\t\t}\n\t\t}\n\t}(&p.wge)\n\n\treturn &p\n}\n\nfunc (p *WorkerPool) Add(job jobFn) {\n\tp.semC <- struct{}{}\n\tp.wg.Add(1)\n\tgo func(ctx context.Context, wg *sync.WaitGroup, semC <-chan struct{}, errC chan<- error) {\n\t\tdefer func() {\n\t\t\t<-semC\n\t\t\twg.Done()\n\t\t}()\n\t\tif err := job(ctx); err != nil {\n\t\t\tslog.Error(\"Worker error\", slogs.Error, err)\n\t\t\terrC <- err\n\t\t}\n\t}(p.ctx, &p.wg, p.semC, p.errC)\n}\n\nfunc (p *WorkerPool) Drain() []error {\n\tif p.cancelFn != nil {\n\t\tp.cancelFn()\n\t\tp.cancelFn = nil\n\t}\n\tp.wg.Wait()\n\tclose(p.semC)\n\tclose(p.errC)\n\tp.wge.Wait()\n\n\tp.mx.RLock()\n\tdefer p.mx.RUnlock()\n\treturn p.errs\n}\n"
  },
  {
    "path": "internal/pool_test.go",
    "content": "package internal_test\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync/atomic\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestWorkerPoolPlain(t *testing.T) {\n\tp := internal.NewWorkerPool(context.Background(), 2)\n\n\tvar c atomic.Int32\n\tfor range 10 {\n\t\tp.Add(func(ctx context.Context) error {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tfmt.Println(\"Worker canceled\")\n\t\t\t\treturn nil\n\t\t\tdefault:\n\t\t\t\tc.Add(1)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t})\n\t}\n\terrs := p.Drain()\n\tassert.Equal(t, 10, int(c.Load()))\n\tassert.Empty(t, errs)\n}\n\nfunc TestWorkerPoolWithError(t *testing.T) {\n\tctx := context.Background()\n\tp := internal.NewWorkerPool(ctx, 2)\n\n\tvar c atomic.Int32\n\tfor i := range 10 {\n\t\tp.Add(func(ctx context.Context) error {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\tfmt.Println(\"Worker canceled\")\n\t\t\t\treturn nil\n\t\t\tdefault:\n\t\t\t\tif i%2 == 0 {\n\t\t\t\t\treturn fmt.Errorf(\"BOOM%d\", i)\n\t\t\t\t}\n\t\t\t\tc.Add(1)\n\t\t\t\treturn nil\n\t\t\t}\n\t\t})\n\t}\n\terrs := p.Drain()\n\tassert.Equal(t, 5, int(c.Load()))\n\tassert.Len(t, errs, 5)\n}\n"
  },
  {
    "path": "internal/port/ann.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage port\n\nimport (\n\t\"errors\"\n)\n\ntype Annotations map[string]string\n\nfunc (a Annotations) PreferredPorts(specs ContainerPortSpecs) (PFAnns, error) {\n\tif len(specs) == 0 {\n\t\treturn nil, errors.New(\"no exposed ports\")\n\t}\n\n\tvalue, ok := a[K9sPortForwardsKey]\n\tif !ok {\n\t\treturn PFAnns{specs[0].ToPFAnn()}, nil\n\t}\n\n\treturn specs.MatchAnnotations(value), nil\n}\n"
  },
  {
    "path": "internal/port/ann_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage port_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/port\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestPreferredPorts(t *testing.T) {\n\tuu := map[string]struct {\n\t\tanns  port.Annotations\n\t\tspecs port.ContainerPortSpecs\n\t\terr   error\n\t\te     string\n\t}{\n\t\t\"no-ports\": {\n\t\t\tanns: port.Annotations{\n\t\t\t\tport.K9sPortForwardsKey: \"c1::4321:p1\",\n\t\t\t},\n\t\t\terr: errors.New(\"no exposed ports\"),\n\t\t},\n\t\t\"no-annotations\": {\n\t\t\tspecs: port.ContainerPortSpecs{\n\t\t\t\t{Container: \"c1\", PortName: \"p1\", PortNum: \"1234\"},\n\t\t\t},\n\t\t\te: \"c1::1234:p1\",\n\t\t},\n\t\t\"single-numb\": {\n\t\t\tanns: port.Annotations{\n\t\t\t\tport.K9sPortForwardsKey: \"c1::4321:1234\",\n\t\t\t},\n\t\t\tspecs: port.ContainerPortSpecs{\n\t\t\t\t{Container: \"c1\", PortName: \"p1\", PortNum: \"1234\"},\n\t\t\t},\n\t\t\te: \"c1::4321:1234/1234\",\n\t\t},\n\t\t\"single-same\": {\n\t\t\tanns: port.Annotations{\n\t\t\t\tport.K9sPortForwardsKey: \"c1::1234\",\n\t\t\t},\n\t\t\tspecs: port.ContainerPortSpecs{\n\t\t\t\t{Container: \"c1\", PortName: \"p1\", PortNum: \"1234\"},\n\t\t\t},\n\t\t\te: \"c1::1234:1234/1234\",\n\t\t},\n\t\t\"single-mismatch\": {\n\t\t\tanns: port.Annotations{\n\t\t\t\tport.K9sPortForwardsKey: \"c2::4321:p1\",\n\t\t\t},\n\t\t\tspecs: port.ContainerPortSpecs{\n\t\t\t\t{Container: \"c1\", PortName: \"p1\", PortNum: \"1234\"},\n\t\t\t},\n\t\t},\n\t\t\"multi\": {\n\t\t\tanns: port.Annotations{\n\t\t\t\tport.K9sPortForwardsKey: \"c1::4321:1234,c1::5432:2345\",\n\t\t\t},\n\t\t\tspecs: port.ContainerPortSpecs{\n\t\t\t\t{Container: \"c1\", PortName: \"p1\", PortNum: \"1234\"},\n\t\t\t\t{Container: \"c1\", PortName: \"p2\", PortNum: \"2345\"},\n\t\t\t},\n\t\t\te: \"c1::4321:1234/1234,c1::5432:2345/2345\",\n\t\t},\n\t\t\"multi-mismatch\": {\n\t\t\tanns: port.Annotations{\n\t\t\t\tport.K9sPortForwardsKey: \"c1::4321:1234,c1::5432:2345\",\n\t\t\t},\n\t\t\tspecs: port.ContainerPortSpecs{\n\t\t\t\t{Container: \"c1\", PortName: \"p1\", PortNum: \"1234\"},\n\t\t\t\t{Container: \"c2\", PortName: \"p3\", PortNum: \"2345\"},\n\t\t\t},\n\t\t\te: \"c1::4321:1234/1234\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tanns, err := u.anns.PreferredPorts(u.specs)\n\t\t\tassert.Equal(t, u.err, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tpfs, err := port.ParsePFs(u.e)\n\t\t\tif err != nil {\n\t\t\t\tpfs = port.PFAnns{}\n\t\t\t}\n\t\t\tassert.Equal(t, pfs, anns)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/port/co_portspec.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage port\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/util/intstr\"\n)\n\n// ContainerPortSpecs represents a container exposed ports.\ntype ContainerPortSpecs []ContainerPortSpec\n\nfunc (c ContainerPortSpecs) Dump() string {\n\tss := make([]string, 0, len(c))\n\tfor _, spec := range c {\n\t\tss = append(ss, spec.String())\n\t}\n\n\treturn strings.Join(ss, \"\\n\")\n}\n\n// MatchSpec checks if given port matches a spec.\nfunc (c ContainerPortSpecs) MatchSpec(s string) bool {\n\t// Skip validation if No port are exposed or no container port spec.\n\tif len(c) == 0 || !strings.Contains(s, \"::\") {\n\t\treturn true\n\t}\n\tfor _, spec := range c {\n\t\tif spec.MatchSpec(s) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// ToTunnels convert port specs to tunnels.\nfunc (c ContainerPortSpecs) ToTunnels(address string) PortTunnels {\n\ttt := make(PortTunnels, 0, len(c))\n\tfor _, spec := range c {\n\t\ttt = append(tt, spec.ToTunnel(address))\n\t}\n\n\treturn tt\n}\n\n// Find finds a matching container port.\nfunc (c ContainerPortSpecs) Find(pf *PFAnn) (ContainerPortSpec, bool) {\n\tfor _, spec := range c {\n\t\tif spec.Match(pf) {\n\t\t\treturn spec, true\n\t\t}\n\t}\n\n\treturn ContainerPortSpec{}, false\n}\n\n// Match checks if container ports match a pf annotation.\nfunc (c ContainerPortSpecs) Match(pf *PFAnn) bool {\n\tfor _, spec := range c {\n\t\tif spec.Match(pf) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (c ContainerPortSpecs) MatchAnnotations(s string) PFAnns {\n\tpfs, err := ParsePFs(s)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tmm := make(PFAnns, 0, len(c))\n\tfor _, pf := range pfs {\n\t\tif pf.Match(c) {\n\t\t\tmm = append(mm, pf)\n\t\t}\n\t}\n\n\treturn mm\n}\n\n// FromContainerPorts hydrates from a pod container specification.\nfunc FromContainerPorts(co string, pp []v1.ContainerPort) ContainerPortSpecs {\n\tspecs := make(ContainerPortSpecs, 0, len(pp))\n\tfor _, p := range pp {\n\t\tif p.Protocol != v1.ProtocolTCP {\n\t\t\tcontinue\n\t\t}\n\t\tspecs = append(specs, NewPortSpec(co, p.Name, p.ContainerPort))\n\t}\n\n\treturn specs\n}\n\n// ContainerPortSpec represents a container port specification.\ntype ContainerPortSpec struct {\n\tContainer string\n\tPortName  string\n\tPortNum   string\n}\n\n// NewPortSpec returns a new instance.\nfunc NewPortSpec(co, portName string, port int32) ContainerPortSpec {\n\treturn ContainerPortSpec{\n\t\tContainer: co,\n\t\tPortName:  portName,\n\t\tPortNum:   strconv.Itoa(int(port)),\n\t}\n}\n\nfunc (c ContainerPortSpec) MatchSpec(s string) bool {\n\ttokens := strings.Split(s, \"::\")\n\tif len(tokens) < 2 {\n\t\treturn false\n\t}\n\n\treturn tokens[0] == c.Container && tokens[1] == c.PortNum\n}\n\nfunc (c ContainerPortSpec) ToTunnel(address string) PortTunnel {\n\treturn PortTunnel{\n\t\tAddress:       address,\n\t\tLocalPort:     c.PortNum,\n\t\tContainerPort: c.PortNum,\n\t}\n}\n\nfunc (c ContainerPortSpec) Port() intstr.IntOrString {\n\tif c.PortName != \"\" {\n\t\treturn intstr.Parse(c.PortName)\n\t}\n\n\treturn intstr.Parse(c.PortNum)\n}\n\nfunc (c ContainerPortSpec) ToPFAnn() *PFAnn {\n\treturn &PFAnn{\n\t\tContainer:     c.Container,\n\t\tContainerPort: c.Port(),\n\t\tLocalPort:     c.PortNum,\n\t}\n}\n\n// Match checks if the container spec matches an annotation.\nfunc (c ContainerPortSpec) Match(ann *PFAnn) bool {\n\tif c.Container != ann.Container {\n\t\treturn false\n\t}\n\n\tswitch ann.ContainerPort.Type {\n\tcase intstr.String:\n\t\treturn c.PortName == ann.ContainerPort.String()\n\tcase intstr.Int:\n\t\treturn c.PortNum == ann.ContainerPort.String()\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// String dumps spec to string.\nfunc (c ContainerPortSpec) String() string {\n\ts := c.Container + \"::\" + c.PortNum\n\tif c.PortName != \"\" {\n\t\ts += \"(\" + c.PortName + \")\"\n\t}\n\treturn s\n}\n"
  },
  {
    "path": "internal/port/co_portspec_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage port_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/port\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestContainerPortSpecMatch(t *testing.T) {\n\tuu := map[string]struct {\n\t\tann  string\n\t\tspec port.ContainerPortSpec\n\t\te    bool\n\t}{\n\t\t\"full\": {\n\t\t\tann: \"c1::4321:1234\",\n\t\t\tspec: port.ContainerPortSpec{\n\t\t\t\tContainer: \"c1\",\n\t\t\t\tPortNum:   \"1234\",\n\t\t\t},\n\t\t\te: true,\n\t\t},\n\t\t\"no-port-name\": {\n\t\t\tann: \"c1::4321:p1/1234\",\n\t\t\tspec: port.ContainerPortSpec{\n\t\t\t\tContainer: \"c1\",\n\t\t\t\tPortName:  \"p1\",\n\t\t\t\tPortNum:   \"1234\",\n\t\t\t},\n\t\t\te: true,\n\t\t},\n\t\t\"port-name-hosed\": {\n\t\t\tann: \"c1::4321:blee/1234\",\n\t\t\tspec: port.ContainerPortSpec{\n\t\t\t\tContainer: \"c1\",\n\t\t\t\tPortName:  \"fred\",\n\t\t\t\tPortNum:   \"1234\",\n\t\t\t},\n\t\t},\n\t\t\"container-name-hosed\": {\n\t\t\tann: \"c2::4321:fred/1234\",\n\t\t\tspec: port.ContainerPortSpec{\n\t\t\t\tContainer: \"c1\",\n\t\t\t\tPortName:  \"blee\",\n\t\t\t\tPortNum:   \"1234\",\n\t\t\t},\n\t\t},\n\t\t\"port-num-hosed\": {\n\t\t\tann: \"c2::4321:1235\",\n\t\t\tspec: port.ContainerPortSpec{\n\t\t\t\tContainer: \"c1\",\n\t\t\t\tPortNum:   \"1234\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tpf, err := port.ParsePF(u.ann)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tassert.Equal(t, u.e, u.spec.Match(pf))\n\t\t})\n\t}\n}\n\nfunc TestContainerPortSpecString(t *testing.T) {\n\tuu := map[string]struct {\n\t\tspec port.ContainerPortSpec\n\t\te    string\n\t}{\n\t\t\"full\": {\n\t\t\tspec: port.NewPortSpec(\"c1\", \"p1\", 1234),\n\t\t\te:    \"c1::1234(p1)\",\n\t\t},\n\t\t\"no-name\": {\n\t\t\tspec: port.NewPortSpec(\"c1\", \"\", 1234),\n\t\t\te:    \"c1::1234\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.spec.String())\n\t\t})\n\t}\n}\n\nfunc TestContainerPortSpecsMatch(t *testing.T) {\n\tuu := map[string]struct {\n\t\tann   string\n\t\tspecs port.ContainerPortSpecs\n\t\te     bool\n\t}{\n\t\t\"full\": {\n\t\t\tann: \"c1::4321:p1\",\n\t\t\tspecs: port.ContainerPortSpecs{\n\t\t\t\tport.NewPortSpec(\"c1\", \"p1\", 1234),\n\t\t\t\tport.NewPortSpec(\"c2\", \"p2\", 1235),\n\t\t\t},\n\t\t\te: true,\n\t\t},\n\t\t\"no-name\": {\n\t\t\tann: \"c1::4321\",\n\t\t\tspecs: port.ContainerPortSpecs{\n\t\t\t\tport.NewPortSpec(\"c1\", \"\", 4321),\n\t\t\t\tport.NewPortSpec(\"c2\", \"p2\", 1235),\n\t\t\t},\n\t\t\te: true,\n\t\t},\n\t\t\"name-hosed\": {\n\t\t\tann: \"c1::4321:p4\",\n\t\t\tspecs: port.ContainerPortSpecs{\n\t\t\t\tport.NewPortSpec(\"c1\", \"p1\", 1234),\n\t\t\t\tport.NewPortSpec(\"c2\", \"p2\", 1235),\n\t\t\t},\n\t\t},\n\t\t\"numb-hosed\": {\n\t\t\tann: \"c1::4321:1235\",\n\t\t\tspecs: port.ContainerPortSpecs{\n\t\t\t\tport.NewPortSpec(\"c1\", \"p1\", 1234),\n\t\t\t\tport.NewPortSpec(\"c2\", \"p2\", 1236),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tpf, err := port.ParsePF(u.ann)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, u.e, u.specs.Match(pf))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/port/pf.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage port\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"k8s.io/apimachinery/pkg/util/intstr\"\n)\n\nconst (\n\t// K9sAutoPortForwardsKey represents an auto portforwards annotation.\n\tK9sAutoPortForwardsKey = \"k9scli.io/auto-port-forwards\"\n\n\t// K9sPortForwardsKey represents a portforwards annotation.\n\tK9sPortForwardsKey = \"k9scli.io/port-forwards\"\n)\n\nvar (\n\tpfRX      = regexp.MustCompile(`\\A([\\w-]+)::(\\d*):?(\\d*|[\\w-]*)/?(\\d+)?\\z`)\n\tpfPlainRX = regexp.MustCompile(`\\A(\\d*):?(\\d*|[\\w-]*)\\z`)\n)\n\n// PFAnn represents a portforward annotation value.\n// Shape: container/portname|portNum:localPort\ntype PFAnn struct {\n\tContainer        string\n\tContainerPort    intstr.IntOrString\n\tLocalPort        string\n\tcontainerPortNum string\n}\n\nfunc ParsePlainPF(ann string) (*PFAnn, error) {\n\tif ann == \"\" {\n\t\treturn nil, fmt.Errorf(\"invalid annotation %q\", ann)\n\t}\n\tvar pf PFAnn\n\tmm := pfPlainRX.FindStringSubmatch(strings.TrimSpace(ann))\n\tif len(mm) < 3 {\n\t\treturn nil, fmt.Errorf(\"invalid plain port-forward %s\", ann)\n\t}\n\tif mm[2] == \"\" {\n\t\tpf.ContainerPort = intstr.Parse(mm[1])\n\t\tpf.LocalPort = mm[1]\n\t\treturn &pf, nil\n\t}\n\tpf.LocalPort, pf.ContainerPort = mm[1], intstr.Parse(mm[2])\n\n\treturn &pf, nil\n}\n\n// ParsePF hydrate a portforward annotation from string.\nfunc ParsePF(ann string) (*PFAnn, error) {\n\tif pf, err := ParsePlainPF(ann); err == nil {\n\t\treturn pf, nil\n\t}\n\tvar pf PFAnn\n\tif mm := pfPlainRX.FindStringSubmatch(strings.TrimSpace(ann)); len(mm) == 3 {\n\t\tpf.containerPortNum = mm[0]\n\t}\n\tr := pfRX.FindStringSubmatch(strings.TrimSpace(ann))\n\tif len(r) < 4 {\n\t\treturn &pf, fmt.Errorf(\"invalid port-forward specification %s\", ann)\n\t}\n\tpf.Container = r[1]\n\tpf.LocalPort, pf.ContainerPort = r[2], intstr.Parse(r[3])\n\tif r[3] == \"\" {\n\t\tpf.ContainerPort = intstr.Parse(pf.LocalPort)\n\t}\n\n\t// Testing only!\n\tif len(r) == 5 && r[4] != \"\" {\n\t\tpf.containerPortNum = r[4]\n\t}\n\tif pf.LocalPort == \"\" {\n\t\tpf.LocalPort = pf.containerPortNum\n\t}\n\n\treturn &pf, nil\n}\n\n// Match checks if annotation matches any of the container ports.\nfunc (p *PFAnn) Match(ss ContainerPortSpecs) bool {\n\tfor _, s := range ss {\n\t\tif s.Match(p) {\n\t\t\tp.containerPortNum = s.PortNum\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (p *PFAnn) AsSpec() string {\n\ts := p.Container + \"::\"\n\tif p.containerPortNum != \"\" {\n\t\treturn s + p.containerPortNum\n\t}\n\treturn s + p.LocalPort\n}\n\n// String dumps the annotation.\nfunc (p *PFAnn) String() string {\n\treturn p.Container + \"::\" + p.LocalPort + \":\" + p.containerPortNum\n}\n\nfunc (p *PFAnn) PortNum() (string, error) {\n\tif p.ContainerPort.Type == intstr.Int {\n\t\treturn p.ContainerPort.String(), nil\n\t}\n\tif p.containerPortNum != \"\" {\n\t\treturn p.containerPortNum, nil\n\t}\n\n\treturn \"\", errors.New(\"no port number assigned\")\n}\n\nfunc (p *PFAnn) ToTunnel(address string) (PortTunnel, error) {\n\tvar pt PortTunnel\n\tport, err := p.PortNum()\n\tif err != nil {\n\t\treturn pt, err\n\t}\n\n\tpt.Address, pt.Container = address, p.Container\n\tpt.ContainerPort, pt.LocalPort = port, p.LocalPort\n\n\treturn pt, nil\n}\n"
  },
  {
    "path": "internal/port/pf_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage port_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/port\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/apimachinery/pkg/util/intstr\"\n)\n\nfunc TestParsePF(t *testing.T) {\n\tuu := map[string]struct {\n\t\texp           string\n\t\tcontainer     string\n\t\tcontainerPort intstr.IntOrString\n\t\tlocalPort     string\n\t\te             error\n\t}{\n\t\t\"full-numbs\": {\n\t\t\texp:           \"c1::4321:1234\",\n\t\t\tcontainer:     \"c1\",\n\t\t\tcontainerPort: intstr.Parse(\"1234\"),\n\t\t\tlocalPort:     \"4321\",\n\t\t},\n\t\t\"full-named\": {\n\t\t\texp:           \"c1::4321:p1/1234\",\n\t\t\tcontainer:     \"c1\",\n\t\t\tcontainerPort: intstr.Parse(\"p1\"),\n\t\t\tlocalPort:     \"4321\",\n\t\t},\n\t\t\"just-named\": {\n\t\t\texp:           \"c1::p1/1234\",\n\t\t\tcontainer:     \"c1\",\n\t\t\tcontainerPort: intstr.Parse(\"p1\"),\n\t\t\tlocalPort:     \"1234\",\n\t\t},\n\t\t\"just-num\": {\n\t\t\texp:           \"c1::1234\",\n\t\t\tcontainer:     \"c1\",\n\t\t\tcontainerPort: intstr.Parse(\"1234\"),\n\t\t\tlocalPort:     \"1234\",\n\t\t},\n\t\t\"plain-single\": {\n\t\t\texp:           \"1234\",\n\t\t\tcontainer:     \"\",\n\t\t\tcontainerPort: intstr.Parse(\"1234\"),\n\t\t\tlocalPort:     \"1234\",\n\t\t},\n\t\t\"plain-full\": {\n\t\t\texp:           \"4321:1234\",\n\t\t\tcontainer:     \"\",\n\t\t\tcontainerPort: intstr.Parse(\"1234\"),\n\t\t\tlocalPort:     \"4321\",\n\t\t},\n\t\t\"toast\": {\n\t\t\texp: \"c1:4321:1234\",\n\t\t\te:   errors.New(\"invalid port-forward specification c1:4321:1234\"),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tpf, err := port.ParsePF(u.exp)\n\t\t\tassert.Equal(t, u.e, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equal(t, u.container, pf.Container)\n\t\t\tassert.Equal(t, u.containerPort, pf.ContainerPort)\n\t\t\tassert.Equal(t, u.localPort, pf.LocalPort)\n\t\t})\n\t}\n}\n\nfunc TestPFMatch(t *testing.T) {\n\tuu := map[string]struct {\n\t\texp   string\n\t\tspecs port.ContainerPortSpecs\n\t\terr   error\n\t\te     bool\n\t}{\n\t\t\"match\": {\n\t\t\texp: \"c1::1234\",\n\t\t\tspecs: port.ContainerPortSpecs{\n\t\t\t\t{Container: \"c1\", PortNum: \"1234\"},\n\t\t\t},\n\t\t\te: true,\n\t\t},\n\t\t\"match-portnum\": {\n\t\t\texp: \"c1::4321:1234\",\n\t\t\tspecs: port.ContainerPortSpecs{\n\t\t\t\t{Container: \"c1\", PortNum: \"1234\"},\n\t\t\t},\n\t\t\te: true,\n\t\t},\n\t\t\"no-match\": {\n\t\t\texp: \"c1::1235\",\n\t\t\tspecs: port.ContainerPortSpecs{\n\t\t\t\t{Container: \"c1\", PortNum: \"1234\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tpf, err := port.ParsePF(u.exp)\n\t\t\tassert.Equal(t, u.err, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equal(t, u.e, pf.Match(u.specs))\n\t\t})\n\t}\n}\n\nfunc TestPFPortNum(t *testing.T) {\n\tuu := map[string]struct {\n\t\texp string\n\t\terr error\n\t\te   string\n\t}{\n\t\t\"port-name\": {\n\t\t\texp: \"c1::4321:1234\",\n\t\t\te:   \"1234\",\n\t\t},\n\t\t\"port-number\": {\n\t\t\texp: \"c1::4321:1234\",\n\t\t\te:   \"1234\",\n\t\t},\n\t\t\"missing-port-number\": {\n\t\t\texp: \"c1::p1\",\n\t\t\terr: errors.New(\"no port number assigned\"),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tpf, err := port.ParsePF(u.exp)\n\t\t\trequire.NoError(t, err)\n\t\t\tn, err := pf.PortNum()\n\t\t\tassert.Equal(t, u.err, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equal(t, u.e, n)\n\t\t})\n\t}\n}\n\nfunc TestPFToTunnel(t *testing.T) {\n\tuu := map[string]struct {\n\t\texp string\n\t\terr error\n\t\te   port.PortTunnel\n\t}{\n\t\t\"port-name\": {\n\t\t\texp: \"c1::p1/1234\",\n\t\t\te: port.PortTunnel{\n\t\t\t\tAddress:       \"blee\",\n\t\t\t\tContainer:     \"c1\",\n\t\t\t\tLocalPort:     \"1234\",\n\t\t\t\tContainerPort: \"1234\",\n\t\t\t},\n\t\t},\n\t\t\"port-numb\": {\n\t\t\texp: \"c1::4321:1234\",\n\t\t\te: port.PortTunnel{\n\t\t\t\tAddress:       \"blee\",\n\t\t\t\tContainer:     \"c1\",\n\t\t\t\tLocalPort:     \"4321\",\n\t\t\t\tContainerPort: \"1234\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tpf, err := port.ParsePF(u.exp)\n\t\t\trequire.NoError(t, err)\n\t\t\tpt, err := pf.ToTunnel(\"blee\")\n\t\t\tassert.Equal(t, u.err, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equal(t, u.e, pt)\n\t\t})\n\t}\n}\n\nfunc TestPFString(t *testing.T) {\n\tuu := map[string]struct {\n\t\texp string\n\t\terr error\n\t\te   string\n\t}{\n\t\t\"port-name\": {\n\t\t\texp: \"c1::p1/1234\",\n\t\t\te:   \"c1::1234:1234\",\n\t\t},\n\t\t\"port-numb\": {\n\t\t\texp: \"c1::4321:1234/1234\",\n\t\t\te:   \"c1::4321:1234\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tpf, err := port.ParsePF(u.exp)\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, u.e, pf.String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/port/pfs.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage port\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n)\n\n// PortCheck checks if port is free on host.\ntype PortChecker func(context.Context, PortTunnel) bool\n\n// PFAnns represents a collection of port forward annotations.\ntype PFAnns []*PFAnn\n\n// ToPortSpec returns a container port and local port definitions.\nfunc (aa PFAnns) ToPortSpec(pp ContainerPortSpecs) (ports, localPorts string) {\n\tspecs, lps := make([]string, 0, len(aa)), make([]string, 0, len(aa))\n\tfor _, a := range aa {\n\t\tspecs = append(specs, a.AsSpec())\n\t\tif a.LocalPort == \"\" {\n\t\t\tif spec, ok := pp.Find(a); ok {\n\t\t\t\ta.LocalPort = spec.PortNum\n\t\t\t}\n\t\t}\n\t\tif a.LocalPort != \"\" {\n\t\t\tlps = append(lps, a.LocalPort)\n\t\t}\n\t}\n\n\treturn strings.Join(specs, \",\"), strings.Join(lps, \",\")\n}\n\nfunc (aa PFAnns) ToTunnels(address string, _ ContainerPortSpecs, available PortChecker) (PortTunnels, error) {\n\tpts := make(PortTunnels, 0, len(aa))\n\tfor _, a := range aa {\n\t\tpt, err := a.ToTunnel(address)\n\t\tif err != nil {\n\t\t\treturn pts, err\n\t\t}\n\t\tif !available(context.Background(), pt) {\n\t\t\treturn pts, fmt.Errorf(\"port %s is not available on host\", pt.LocalPort)\n\t\t}\n\t\tpts = append(pts, pt)\n\t}\n\n\treturn pts, nil\n}\n\n// ParsePFs hydrates a collection of portforward annotations.\nfunc ParsePFs(ann string) (PFAnns, error) {\n\tss := strings.Split(ann, \",\")\n\tpp := make(PFAnns, 0, len(ss))\n\tfor _, s := range ss {\n\t\tf, err := ParsePF(s)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpp = append(pp, f)\n\t}\n\n\treturn pp, nil\n}\n\nfunc ToTunnels(address, specs, localPorts string) (PortTunnels, error) {\n\tpp, lps := strings.Split(specs, \",\"), strings.Split(localPorts, \",\")\n\n\tif len(pp) != len(lps) {\n\t\treturn nil, fmt.Errorf(\"spec to local port count mismatch. Expected %d but got %d\", len(pp), len(lps))\n\t}\n\n\tpts := make(PortTunnels, 0, len(pp))\n\tfor i, p := range pp {\n\t\ta, err := ParsePF(p)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tn, err := a.PortNum()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tpts = append(pts, PortTunnel{\n\t\t\tAddress:       address,\n\t\t\tContainer:     a.Container,\n\t\t\tContainerPort: n,\n\t\t\tLocalPort:     lps[i],\n\t\t})\n\t}\n\n\treturn pts, nil\n}\n"
  },
  {
    "path": "internal/port/pfs_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage port_test\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/port\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/apimachinery/pkg/util/intstr\"\n)\n\nfunc TestParsePFs(t *testing.T) {\n\tuu := map[string]struct {\n\t\tspec string\n\t\tpfs  port.PFAnns\n\t\te    error\n\t}{\n\t\t\"single\": {\n\t\t\tspec: \"c2::4321:1234\",\n\t\t\tpfs: port.PFAnns{\n\t\t\t\t{Container: \"c2\", ContainerPort: intstr.Parse(\"1234\"), LocalPort: \"4321\"},\n\t\t\t},\n\t\t},\n\t\t\"multi\": {\n\t\t\tspec: \"c1::4321:1234,c2::6666:6543\",\n\t\t\tpfs: port.PFAnns{\n\t\t\t\t{Container: \"c1\", ContainerPort: intstr.Parse(\"1234\"), LocalPort: \"4321\"},\n\t\t\t\t{Container: \"c2\", ContainerPort: intstr.Parse(\"6543\"), LocalPort: \"6666\"},\n\t\t\t},\n\t\t},\n\t\t\"spaces\": {\n\t\t\tspec: \" c1::4321:1234 , c2::6666:6543 \",\n\t\t\tpfs: port.PFAnns{\n\t\t\t\t{Container: \"c1\", ContainerPort: intstr.Parse(\"1234\"), LocalPort: \"4321\"},\n\t\t\t\t{Container: \"c2\", ContainerPort: intstr.Parse(\"6543\"), LocalPort: \"6666\"},\n\t\t\t},\n\t\t},\n\t\t\"plain-multi\": {\n\t\t\tspec: \"4321:1234, 6666:6543\",\n\t\t\tpfs: port.PFAnns{\n\t\t\t\t{ContainerPort: intstr.Parse(\"1234\"), LocalPort: \"4321\"},\n\t\t\t\t{ContainerPort: intstr.Parse(\"6543\"), LocalPort: \"6666\"},\n\t\t\t},\n\t\t},\n\t\t\"toast\": {\n\t\t\tspec: \"c1::p1:1234,c2::4321\",\n\t\t\te:    errors.New(\"invalid port-forward specification c1::p1:1234\"),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tpfs, err := port.ParsePFs(u.spec)\n\t\t\tassert.Equal(t, u.e, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equal(t, u.pfs, pfs)\n\t\t})\n\t}\n}\n\nfunc TestPFsToTunnel(t *testing.T) {\n\tuu := map[string]struct {\n\t\texp   string\n\t\tspecs port.ContainerPortSpecs\n\t\tpts   port.PortTunnels\n\t\te     error\n\t}{\n\t\t\"single\": {\n\t\t\texp: \"c2::4321:1234\",\n\t\t\tspecs: port.ContainerPortSpecs{\n\t\t\t\t{Container: \"c2\", PortName: \"p1\", PortNum: \"1234\"},\n\t\t\t},\n\t\t\tpts: port.PortTunnels{\n\t\t\t\t{Address: \"fred\", Container: \"c2\", ContainerPort: \"1234\", LocalPort: \"4321\"},\n\t\t\t},\n\t\t},\n\t\t\"hosed\": {\n\t\t\texp: \"c2::p2\",\n\t\t\tspecs: port.ContainerPortSpecs{\n\t\t\t\t{Container: \"c2\", PortName: \"p1\", PortNum: \"1234\"},\n\t\t\t},\n\t\t\tpts: port.PortTunnels{\n\t\t\t\t{Address: \"fred\", Container: \"c2\", ContainerPort: \"1234\", LocalPort: \"4321\"},\n\t\t\t},\n\t\t\te: errors.New(\"no port number assigned\"),\n\t\t},\n\t}\n\n\tf := func(context.Context, port.PortTunnel) bool {\n\t\treturn true\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tpfs, err := port.ParsePFs(u.exp)\n\t\t\trequire.NoError(t, err)\n\t\t\tpts, err := pfs.ToTunnels(\"fred\", u.specs, f)\n\t\t\tassert.Equal(t, u.e, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equal(t, u.pts, pts)\n\t\t})\n\t}\n}\n\nfunc TestPFsToPortSpec(t *testing.T) {\n\tuu := map[string]struct {\n\t\texp        string\n\t\tspec, port string\n\t\tspecs      port.ContainerPortSpecs\n\t\te          error\n\t}{\n\t\t\"single\": {\n\t\t\texp:  \"c2::4321:p2/1234\",\n\t\t\tspec: \"c2::1234\",\n\t\t\tport: \"4321\",\n\t\t\tspecs: port.ContainerPortSpecs{\n\t\t\t\t{Container: \"c2\", PortNum: \"1234\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tpfs, err := port.ParsePFs(u.exp)\n\t\t\tassert.Equal(t, u.e, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tspec, prt := pfs.ToPortSpec(u.specs)\n\t\t\tassert.Equal(t, u.spec, spec)\n\t\t\tassert.Equal(t, u.port, prt)\n\t\t})\n\t}\n}\n\nfunc TestToTunnels(t *testing.T) {\n\tuu := map[string]struct {\n\t\tspecs, ports string\n\t\ttunnels      port.PortTunnels\n\t\terr          error\n\t}{\n\t\t\"single\": {\n\t\t\tspecs: \"c2::4321:p2/1234\",\n\t\t\tports: \"4321\",\n\t\t\ttunnels: port.PortTunnels{\n\t\t\t\t{\n\t\t\t\t\tAddress:       \"blee\",\n\t\t\t\t\tLocalPort:     \"4321\",\n\t\t\t\t\tContainer:     \"c2\",\n\t\t\t\t\tContainerPort: \"1234\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"multi\": {\n\t\t\tspecs: \"c1::5432:2345/2345,c2::4321:p2/1234\",\n\t\t\tports: \"5432,4321\",\n\t\t\ttunnels: port.PortTunnels{\n\t\t\t\t{\n\t\t\t\t\tAddress:       \"blee\",\n\t\t\t\t\tLocalPort:     \"5432\",\n\t\t\t\t\tContainer:     \"c1\",\n\t\t\t\t\tContainerPort: \"2345\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAddress:       \"blee\",\n\t\t\t\t\tLocalPort:     \"4321\",\n\t\t\t\t\tContainer:     \"c2\",\n\t\t\t\t\tContainerPort: \"1234\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\ttt, err := port.ToTunnels(\"blee\", u.specs, u.ports)\n\t\t\tassert.Equal(t, u.err, err)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tassert.Equal(t, u.tunnels, tt)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/port/tunnel.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage port\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"net\"\n\n\t\"github.com/derailed/k9s/internal/slogs\"\n)\n\n// PortTunnels represents a collection of tunnels.\ntype PortTunnels []PortTunnel\n\n// CheckAvailable checks if all port tunnels are available.\nfunc (t PortTunnels) CheckAvailable(ctx context.Context) error {\n\tfor _, pt := range t {\n\t\tif !IsPortFree(ctx, pt) {\n\t\t\treturn fmt.Errorf(\"port %s is not available on host\", pt.LocalPort)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// PortTunnel represents a host tunnel port mapper.\ntype PortTunnel struct {\n\tAddress, Container, LocalPort, ContainerPort string\n}\n\n// NewPortTunnel returns a new instance.\nfunc NewPortTunnel(a, co, lp, cp string) PortTunnel {\n\treturn PortTunnel{\n\t\tAddress:       a,\n\t\tContainer:     co,\n\t\tLocalPort:     lp,\n\t\tContainerPort: cp,\n\t}\n}\n\n// String dumps as string.\nfunc (t PortTunnel) String() string {\n\treturn fmt.Sprintf(\"%s|%s|%s:%s\", t.Address, t.Container, t.LocalPort, t.ContainerPort)\n}\n\n// PortMap returns a port mapping.\nfunc (t PortTunnel) PortMap() string {\n\tif t.LocalPort == \"\" {\n\t\tt.LocalPort = t.ContainerPort\n\t}\n\n\treturn t.LocalPort + \":\" + t.ContainerPort\n}\n\n// IsPortFree checks if a address/port pair is available on host.\nfunc IsPortFree(ctx context.Context, t PortTunnel) bool {\n\tvar ncfg net.ListenConfig\n\ts, err := ncfg.Listen(ctx, \"tcp\", fmt.Sprintf(\"%s:%s\", t.Address, t.LocalPort))\n\tif err != nil {\n\t\tslog.Warn(\"Port is not available\", slogs.Port, t.LocalPort, slogs.Address, t.Address)\n\t\treturn false\n\t}\n\n\treturn s.Close() == nil\n}\n"
  },
  {
    "path": "internal/port/tunnel_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage port_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/port\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestPortTunnelMap(t *testing.T) {\n\tuu := map[string]struct {\n\t\tpt              port.PortTunnel\n\t\tcoPort, locPort string\n\t\te               string\n\t}{\n\t\t\"plain\": {\n\t\t\tpt: port.PortTunnel{\n\t\t\t\tAddress:       \"localhost\",\n\t\t\t\tLocalPort:     \"1234\",\n\t\t\t\tContainerPort: \"4321\",\n\t\t\t},\n\t\t\te: \"1234:4321\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.pt.PortMap())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/render/alias.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n)\n\nvar defaultAliasHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"RESOURCE\"},\n\tmodel1.HeaderColumn{Name: \"GROUP\"},\n\tmodel1.HeaderColumn{Name: \"VERSION\"},\n\tmodel1.HeaderColumn{Name: \"COMMAND\"},\n}\n\n// Alias renders an aliases to screen.\ntype Alias struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (Alias) Header(string) model1.Header {\n\treturn defaultAliasHeader\n}\n\n// Render renders a K8s resource to screen.\n// BOZO!! Pass in a row with pre-alloc fields??\nfunc (Alias) Render(o any, _ string, r *model1.Row) error {\n\ta, ok := o.(AliasRes)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected AliasRes, but got %T\", o)\n\t}\n\tslices.Sort(a.Aliases)\n\n\tr.ID = a.GVR.String()\n\tr.Fields = append(r.Fields,\n\t\ta.GVR.R(),\n\t\ta.GVR.G(),\n\t\ta.GVR.V(),\n\t\tstrings.Join(a.Aliases, \" \"),\n\t)\n\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\n// AliasRes represents an alias resource.\ntype AliasRes struct {\n\tGVR     *client.GVR\n\tAliases []string\n}\n\n// GetObjectKind returns a schema object.\nfunc (AliasRes) GetObjectKind() schema.ObjectKind {\n\treturn nil\n}\n\n// DeepCopyObject returns a container copy.\nfunc (a AliasRes) DeepCopyObject() runtime.Object {\n\treturn a\n}\n"
  },
  {
    "path": "internal/render/alias_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestAliasColorer(t *testing.T) {\n\tvar a render.Alias\n\th := model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\tmodel1.HeaderColumn{Name: \"B\"},\n\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t}\n\tr := model1.Row{ID: \"g/v/r\", Fields: model1.Fields{\"r\", \"blee\", \"g\"}}\n\tuu := map[string]struct {\n\t\tns string\n\t\tre model1.RowEvent\n\t\te  tcell.Color\n\t}{\n\t\t\"addAll\": {\n\t\t\tns: client.NamespaceAll,\n\t\t\tre: model1.RowEvent{Kind: model1.EventAdd, Row: r},\n\t\t\te:  tcell.ColorBlue,\n\t\t},\n\t\t\"deleteAll\": {\n\t\t\tns: client.NamespaceAll,\n\t\t\tre: model1.RowEvent{Kind: model1.EventDelete, Row: r},\n\t\t\te:  tcell.ColorGray,\n\t\t},\n\t\t\"updateAll\": {\n\t\t\tns: client.NamespaceAll,\n\t\t\tre: model1.RowEvent{Kind: model1.EventUpdate, Row: r},\n\t\t\te:  tcell.ColorDefault,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, a.ColorerFunc()(u.ns, h, &u.re))\n\t\t})\n\t}\n}\n\nfunc TestAliasHeader(t *testing.T) {\n\th := model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"RESOURCE\"},\n\t\tmodel1.HeaderColumn{Name: \"GROUP\"},\n\t\tmodel1.HeaderColumn{Name: \"VERSION\"},\n\t\tmodel1.HeaderColumn{Name: \"COMMAND\"},\n\t}\n\n\tvar a render.Alias\n\tassert.Equal(t, h, a.Header(\"ns-1\"))\n\tassert.Equal(t, h, a.Header(client.NamespaceAll))\n}\n\nfunc TestAliasRender(t *testing.T) {\n\tvar a render.Alias\n\n\to := render.AliasRes{\n\t\tGVR:     client.NewGVR(\"fred/v1/blee\"),\n\t\tAliases: []string{\"a\", \"b\", \"c\"},\n\t}\n\n\tvar r model1.Row\n\trequire.NoError(t, a.Render(o, \"fred/v1/blee\", &r))\n\tassert.Equal(t, model1.Row{\n\t\tID:     \"fred/v1/blee\",\n\t\tFields: model1.Fields{\"blee\", \"fred\", \"v1\", \"a b c\"},\n\t}, r)\n}\n\nfunc BenchmarkAlias(b *testing.B) {\n\to := render.AliasRes{\n\t\tGVR:     client.NewGVR(\"fred/v1/blee\"),\n\t\tAliases: []string{\"a\", \"b\", \"c\"},\n\t}\n\tvar a render.Alias\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\tfor range b.N {\n\t\tvar r model1.Row\n\t\t_ = a.Render(o, \"ns-1\", &r)\n\t}\n}\n"
  },
  {
    "path": "internal/render/base.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n)\n\n// DecoratorFunc decorates a string.\ntype DecoratorFunc func(string) string\n\n// AgeDecorator represents a timestamped as human column.\nvar AgeDecorator = toAgeHuman\n\ntype Base struct {\n\tvs         *config.ViewSetting\n\tspecs      ColumnSpecs\n\tincludeObj bool\n}\n\nfunc (b *Base) SetIncludeObject(f bool) {\n\tb.includeObj = f\n}\n\n// IsGeneric identifies a generic handler.\nfunc (*Base) IsGeneric() bool {\n\treturn false\n}\n\nfunc (b *Base) doHeader(dh model1.Header) model1.Header {\n\tif b.specs.isEmpty() {\n\t\treturn dh\n\t}\n\n\treturn b.specs.Header(dh)\n}\n\n// SetViewSetting sets custom view settings if any.\nfunc (b *Base) SetViewSetting(vs *config.ViewSetting) {\n\tvar cols []string\n\tb.vs = vs\n\tif vs != nil {\n\t\tcols = vs.Columns\n\t}\n\tspecs, err := NewColsSpecs(cols...).parseSpecs()\n\tif err != nil {\n\t\tslog.Error(\"Unable to grok custom columns\", slogs.Error, err)\n\t\treturn\n\t}\n\tb.specs = specs\n}\n\n// ColorerFunc colors a resource row.\nfunc (*Base) ColorerFunc() model1.ColorerFunc {\n\treturn model1.DefaultColorer\n}\n\n// Happy returns true if resource is happy, false otherwise.\nfunc (*Base) Happy(string, *model1.Row) bool {\n\treturn true\n}\n\n// Healthy checks if the resource is healthy.\nfunc (*Base) Healthy(context.Context, any) error {\n\treturn nil\n}\n"
  },
  {
    "path": "internal/render/benchmark.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n)\n\nvar (\n\ttotalRx = regexp.MustCompile(`Total:\\s+([0-9.]+)\\ssecs`)\n\treqRx   = regexp.MustCompile(`Requests/sec:\\s+([0-9.]+)`)\n\tokRx    = regexp.MustCompile(`\\[2\\d{2}\\]\\s+(\\d+)\\s+responses`)\n\terrRx   = regexp.MustCompile(`\\[[45]\\d{2}\\]\\s+(\\d+)\\s+responses`)\n\ttoastRx = regexp.MustCompile(`Error distribution`)\n)\n\n// Benchmark renders a benchmarks to screen.\ntype Benchmark struct {\n\tBase\n}\n\n// ColorerFunc colors a resource row.\nfunc (Benchmark) ColorerFunc() model1.ColorerFunc {\n\treturn func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color {\n\t\tif !model1.IsValid(ns, h, re.Row) {\n\t\t\treturn model1.ErrColor\n\t\t}\n\n\t\treturn tcell.ColorPaleGreen\n\t}\n}\n\n// Header returns a header row.\nfunc (Benchmark) Header(string) model1.Header {\n\treturn model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\t\tmodel1.HeaderColumn{Name: \"NAME\"},\n\t\tmodel1.HeaderColumn{Name: \"STATUS\"},\n\t\tmodel1.HeaderColumn{Name: \"TIME\"},\n\t\tmodel1.HeaderColumn{Name: \"REQ/S\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\t\tmodel1.HeaderColumn{Name: \"2XX\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\t\tmodel1.HeaderColumn{Name: \"4XX/5XX\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\t\tmodel1.HeaderColumn{Name: \"REPORT\"},\n\t\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\t\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n\t}\n}\n\n// Render renders a K8s resource to screen.\nfunc (b Benchmark) Render(o any, ns string, r *model1.Row) error {\n\tbench, ok := o.(BenchInfo)\n\tif !ok {\n\t\treturn fmt.Errorf(\"no benchmarks available %T\", o)\n\t}\n\n\tdata, err := b.readFile(bench.Path)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"unable to load bench file %s\", bench.Path)\n\t}\n\n\tr.ID = bench.Path\n\tr.Fields = make(model1.Fields, len(b.Header(ns)))\n\tif err := b.initRow(r.Fields, bench.File); err != nil {\n\t\treturn err\n\t}\n\tb.augmentRow(r.Fields, data)\n\tr.Fields[8] = AsStatus(b.diagnose(ns, r.Fields))\n\n\treturn nil\n}\n\n// Happy returns true if resource is happy, false otherwise.\nfunc (Benchmark) diagnose(ns string, ff model1.Fields) error {\n\tstatusCol := 3\n\tif !client.IsAllNamespaces(ns) {\n\t\tstatusCol--\n\t}\n\n\tif len(ff) < statusCol {\n\t\treturn nil\n\t}\n\tif ff[statusCol] != \"pass\" {\n\t\treturn errors.New(\"failed benchmark\")\n\t}\n\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc (Benchmark) readFile(file string) (string, error) {\n\tdata, err := os.ReadFile(file)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(data), nil\n}\n\nfunc (Benchmark) initRow(row model1.Fields, f os.FileInfo) error {\n\ttokens := strings.Split(f.Name(), \"_\")\n\tif len(tokens) < 2 {\n\t\treturn fmt.Errorf(\"invalid file name %s\", f.Name())\n\t}\n\trow[0] = tokens[0]\n\trow[1] = tokens[1]\n\trow[7] = f.Name()\n\trow[9] = ToAge(metav1.Time{Time: f.ModTime()})\n\n\treturn nil\n}\n\nfunc (b Benchmark) augmentRow(fields model1.Fields, data string) {\n\tif data == \"\" {\n\t\treturn\n\t}\n\n\tcol := 2\n\tfields[col] = \"pass\"\n\tmf := toastRx.FindAllStringSubmatch(data, 1)\n\tif len(mf) > 0 {\n\t\tfields[col] = \"fail\"\n\t}\n\tcol++\n\n\tmt := totalRx.FindAllStringSubmatch(data, 1)\n\tif len(mt) > 0 {\n\t\tfields[col] = mt[0][1]\n\t}\n\tcol++\n\n\tmr := reqRx.FindAllStringSubmatch(data, 1)\n\tif len(mr) > 0 {\n\t\tfields[col] = mr[0][1]\n\t}\n\tcol++\n\n\tms := okRx.FindAllStringSubmatch(data, -1)\n\tfields[col] = b.countReq(ms)\n\tcol++\n\n\tme := errRx.FindAllStringSubmatch(data, -1)\n\tfields[col] = b.countReq(me)\n}\n\nfunc (Benchmark) countReq(rr [][]string) string {\n\tif len(rr) == 0 {\n\t\treturn \"0\"\n\t}\n\n\tvar sum int\n\tfor _, m := range rr {\n\t\tif m, err := strconv.Atoi(m[1]); err == nil {\n\t\t\tsum += m\n\t\t}\n\t}\n\treturn AsThousands(int64(sum))\n}\n\n// BenchInfo represents benchmark run info.\ntype BenchInfo struct {\n\tFile os.FileInfo\n\tPath string\n}\n\n// GetObjectKind returns a schema object.\nfunc (BenchInfo) GetObjectKind() schema.ObjectKind {\n\treturn nil\n}\n\n// DeepCopyObject returns a container copy.\nfunc (b BenchInfo) DeepCopyObject() runtime.Object {\n\treturn b\n}\n"
  },
  {
    "path": "internal/render/benchmark_int_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"log/slog\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc init() {\n\tslog.SetDefault(slog.New(slog.DiscardHandler))\n}\n\nfunc TestAugmentRow(t *testing.T) {\n\tuu := map[string]struct {\n\t\tfile string\n\t\te    model1.Fields\n\t}{\n\t\t\"cool\": {\n\t\t\t\"testdata/b1.txt\",\n\t\t\tmodel1.Fields{\"pass\", \"3.3544\", \"29.8116\", \"100\", \"0\"},\n\t\t},\n\t\t\"2XX\": {\n\t\t\t\"testdata/b4.txt\",\n\t\t\tmodel1.Fields{\"pass\", \"3.3544\", \"29.8116\", \"160\", \"0\"},\n\t\t},\n\t\t\"4XX/5XX\": {\n\t\t\t\"testdata/b2.txt\",\n\t\t\tmodel1.Fields{\"pass\", \"3.3544\", \"29.8116\", \"100\", \"12\"},\n\t\t},\n\t\t\"toast\": {\n\t\t\t\"testdata/b3.txt\",\n\t\t\tmodel1.Fields{\"fail\", \"2.3688\", \"35.4606\", \"0\", \"0\"},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tdata, err := os.ReadFile(u.file)\n\n\t\t\trequire.NoError(t, err)\n\t\t\tfields := make(model1.Fields, 8)\n\t\t\tb := Benchmark{}\n\t\t\tb.augmentRow(fields, string(data))\n\t\t\tassert.Equal(t, u.e, fields[2:7])\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/render/cm.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\n// ConfigMap renders a K8s ConfigMap to screen.\ntype ConfigMap struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (m ConfigMap) Header(_ string) model1.Header {\n\treturn m.doHeader(defaultCMHeader)\n}\n\nvar defaultCMHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"DATA\"},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// Render renders a K8s resource to screen.\nfunc (m ConfigMap) Render(o any, _ string, row *model1.Row) error {\n\tif err := m.defaultRow(o, row); err != nil {\n\t\treturn err\n\t}\n\tif m.specs.isEmpty() {\n\t\treturn nil\n\t}\n\n\tcols, err := m.specs.realize(o.(*unstructured.Unstructured), defaultCMHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\n// Render renders a K8s resource to screen.\nfunc (ConfigMap) defaultRow(o any, r *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected *Unstructured, but got %T\", o)\n\t}\n\tvar cm v1.ConfigMap\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &cm)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tr.ID = client.FQN(cm.Namespace, cm.Name)\n\tr.Fields = model1.Fields{\n\t\tcm.Namespace,\n\t\tcm.Name,\n\t\tstrconv.Itoa(len(cm.Data)),\n\t\t\"\",\n\t\tToAge(cm.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/render/container.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n\tmv1beta1 \"k8s.io/metrics/pkg/apis/metrics/v1beta1\"\n)\n\nconst falseStr = \"false\"\n\n// ContainerWithMetrics represents a container and it's metrics.\ntype ContainerWithMetrics interface {\n\t// Container returns the container\n\tContainer() *v1.Container\n\n\t// ContainerStatus returns the current container status.\n\tContainerStatus() *v1.ContainerStatus\n\n\t// Metrics returns the container metrics.\n\tMetrics() *mv1beta1.ContainerMetrics\n\n\t// Age returns the pod age.\n\tAge() metav1.Time\n\n\t// IsInit indicates a init container.\n\tIsInit() bool\n}\n\n// Container renders a K8s Container to screen.\ntype Container struct {\n\tBase\n}\n\n// ColorerFunc colors a resource row.\nfunc (Container) ColorerFunc() model1.ColorerFunc {\n\treturn func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color {\n\t\tc := model1.DefaultColorer(ns, h, re)\n\n\t\tidx, ok := h.IndexOf(\"STATE\", true)\n\t\tif !ok {\n\t\t\treturn c\n\t\t}\n\t\tswitch strings.TrimSpace(re.Row.Fields[idx]) {\n\t\tcase Pending:\n\t\t\treturn model1.PendingColor\n\t\tcase ContainerCreating, PodInitializing:\n\t\t\treturn model1.AddColor\n\t\tcase Terminating, Initialized:\n\t\t\treturn model1.HighlightColor\n\t\tcase Completed:\n\t\t\treturn model1.CompletedColor\n\t\tcase Running:\n\t\t\treturn c\n\t\tdefault:\n\t\t\treturn model1.ErrColor\n\t\t}\n\t}\n}\n\n// Header returns a header row.\nfunc (Container) Header(_ string) model1.Header {\n\treturn defaultCOHeader\n}\n\n// Header returns a header row.\nvar defaultCOHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"IDX\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"PF\"},\n\tmodel1.HeaderColumn{Name: \"IMAGE\"},\n\tmodel1.HeaderColumn{Name: \"READY\"},\n\tmodel1.HeaderColumn{Name: \"STATE\"},\n\tmodel1.HeaderColumn{Name: \"RESTARTS\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"PROBES(L:R:S)\"},\n\tmodel1.HeaderColumn{Name: \"CPU\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"CPU/RL\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"%CPU/R\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"%CPU/L\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"MEM\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"MEM/RL\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"%MEM/R\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"%MEM/L\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"GPU/RL\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"PORTS\"},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// Render renders a K8s resource to screen.\nfunc (c Container) Render(o any, _ string, row *model1.Row) error {\n\tcr, ok := o.(ContainerRes)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected ContainerRes, but got %T\", o)\n\t}\n\n\treturn c.defaultRow(cr, row)\n}\n\nfunc (c Container) defaultRow(cr ContainerRes, r *model1.Row) error {\n\tcur, res := gatherContainerMX(cr.Container, cr.MX)\n\tready, state, restarts := falseStr, MissingValue, \"0\"\n\tif cr.Status != nil {\n\t\tready, state, restarts = boolToStr(cr.Status.Ready), ToContainerState(cr.Status.State), strconv.Itoa(int(cr.Status.RestartCount))\n\t}\n\n\tr.ID = cr.Container.Name\n\tr.Fields = model1.Fields{\n\t\tcr.Idx,\n\t\tcr.Container.Name,\n\t\t\"●\",\n\t\tcr.Container.Image,\n\t\tready,\n\t\tstate,\n\t\trestarts,\n\t\tprobe(cr.Container.LivenessProbe) + \":\" + probe(cr.Container.ReadinessProbe) + \":\" + probe(cr.Container.StartupProbe),\n\t\ttoMc(cur.cpu),\n\t\ttoMc(res.cpu) + \":\" + toMc(res.lcpu),\n\t\tclient.ToPercentageStr(cur.cpu, res.cpu),\n\t\tclient.ToPercentageStr(cur.cpu, res.lcpu),\n\t\ttoMi(cur.mem),\n\t\ttoMi(res.mem) + \":\" + toMi(res.lmem),\n\t\tclient.ToPercentageStr(cur.mem, res.mem),\n\t\tclient.ToPercentageStr(cur.mem, res.lmem),\n\t\ttoMc(res.gpu) + \":\" + toMc(res.lgpu),\n\t\tToContainerPorts(cr.Container.Ports),\n\t\tAsStatus(c.diagnose(state, ready)),\n\t\tToAge(cr.Age),\n\t}\n\n\treturn nil\n}\n\n// Happy returns true if resource is happy, false otherwise.\nfunc (Container) diagnose(state, ready string) error {\n\tif state == \"Completed\" {\n\t\treturn nil\n\t}\n\n\tif ready == falseStr {\n\t\treturn errors.New(\"container is not ready\")\n\t}\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc containerRequests(co *v1.Container) v1.ResourceList {\n\treq := co.Resources.Requests\n\tif len(req) != 0 {\n\t\treturn req\n\t}\n\tlim := co.Resources.Limits\n\tif len(lim) != 0 {\n\t\treturn lim\n\t}\n\n\treturn nil\n}\n\nfunc gatherContainerMX(co *v1.Container, mx *mv1beta1.ContainerMetrics) (c, r metric) {\n\trList, lList := containerRequests(co), co.Resources.Limits\n\n\tif q := rList.Cpu(); q != nil {\n\t\tr.cpu = q.MilliValue()\n\t}\n\tif q := lList.Cpu(); q != nil {\n\t\tr.lcpu = q.MilliValue()\n\t}\n\n\tif q := rList.Memory(); q != nil {\n\t\tr.mem = q.Value()\n\t}\n\tif q := lList.Memory(); q != nil {\n\t\tr.lmem = q.Value()\n\t}\n\n\tif q := extractGPU(rList); q != nil {\n\t\tr.gpu = q.Value()\n\t}\n\tif q := extractGPU(lList); q != nil {\n\t\tr.lgpu = q.Value()\n\t}\n\n\tif mx != nil {\n\t\tif q := mx.Usage.Cpu(); q != nil {\n\t\t\tc.cpu = q.MilliValue()\n\t\t}\n\t\tif q := mx.Usage.Memory(); q != nil {\n\t\t\tc.mem = q.Value()\n\t\t}\n\t}\n\n\treturn\n}\n\n// ToContainerPorts returns container ports as a string.\nfunc ToContainerPorts(pp []v1.ContainerPort) string {\n\tports := make([]string, len(pp))\n\tfor i, p := range pp {\n\t\tif p.Name != \"\" {\n\t\t\tports[i] = p.Name + \":\"\n\t\t}\n\t\tports[i] += strconv.Itoa(int(p.ContainerPort))\n\t\tif p.Protocol != \"TCP\" {\n\t\t\tports[i] += \"╱\" + string(p.Protocol)\n\t\t}\n\t}\n\n\treturn strings.Join(ports, \",\")\n}\n\n// ToContainerState returns container state as a string.\nfunc ToContainerState(s v1.ContainerState) string {\n\tswitch {\n\tcase s.Waiting != nil:\n\t\tif s.Waiting.Reason != \"\" {\n\t\t\treturn s.Waiting.Reason\n\t\t}\n\t\treturn \"Waiting\"\n\n\tcase s.Terminated != nil:\n\t\tif s.Terminated.Reason != \"\" {\n\t\t\treturn s.Terminated.Reason\n\t\t}\n\t\treturn \"Terminating\"\n\tcase s.Running != nil:\n\t\treturn \"Running\"\n\tdefault:\n\t\treturn MissingValue\n\t}\n}\n\nconst (\n\ton  = \"on\"\n\toff = \"off\"\n)\n\nfunc probe(p *v1.Probe) string {\n\tif p == nil {\n\t\treturn off\n\t}\n\treturn on\n}\n\n// ContainerRes represents a container and its metrics.\ntype ContainerRes struct {\n\tContainer *v1.Container\n\tStatus    *v1.ContainerStatus\n\tMX        *mv1beta1.ContainerMetrics\n\tIdx       string\n\tAge       metav1.Time\n}\n\n// GetObjectKind returns a schema object.\nfunc (ContainerRes) GetObjectKind() schema.ObjectKind {\n\treturn nil\n}\n\n// DeepCopyObject returns a container copy.\nfunc (c ContainerRes) DeepCopyObject() runtime.Object {\n\treturn c\n}\n"
  },
  {
    "path": "internal/render/container_int_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/api/resource\"\n\tmv1beta1 \"k8s.io/metrics/pkg/apis/metrics/v1beta1\"\n)\n\nfunc Test_gatherContainerMX(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcontainer v1.Container\n\t\tmx        *mv1beta1.ContainerMetrics\n\t\tc, r      metric\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"amd-request\": {\n\t\t\tcontainer: v1.Container{\n\t\t\t\tName:  \"fred\",\n\t\t\t\tImage: \"img\",\n\t\t\t\tResources: v1.ResourceRequirements{\n\t\t\t\t\tRequests: v1.ResourceList{\n\t\t\t\t\t\tv1.ResourceCPU:    resource.MustParse(\"10m\"),\n\t\t\t\t\t\tv1.ResourceMemory: resource.MustParse(\"20Mi\"),\n\t\t\t\t\t\t\"nvidia.com/gpu\":  resource.MustParse(\"1\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmx: &mv1beta1.ContainerMetrics{\n\t\t\t\tName: \"fred\",\n\t\t\t\tUsage: v1.ResourceList{\n\t\t\t\t\tv1.ResourceCPU:    resource.MustParse(\"10m\"),\n\t\t\t\t\tv1.ResourceMemory: resource.MustParse(\"20Mi\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tc: metric{\n\t\t\t\tcpu: 10,\n\t\t\t\tmem: 20971520,\n\t\t\t},\n\t\t\tr: metric{\n\t\t\t\tcpu: 10,\n\t\t\t\tgpu: 1,\n\t\t\t\tmem: 20971520,\n\t\t\t},\n\t\t},\n\n\t\t\"amd-both\": {\n\t\t\tcontainer: v1.Container{\n\t\t\t\tName:  \"fred\",\n\t\t\t\tImage: \"img\",\n\t\t\t\tResources: v1.ResourceRequirements{\n\t\t\t\t\tRequests: v1.ResourceList{\n\t\t\t\t\t\tv1.ResourceCPU:    resource.MustParse(\"10m\"),\n\t\t\t\t\t\tv1.ResourceMemory: resource.MustParse(\"20Mi\"),\n\t\t\t\t\t\t\"nvidia.com/gpu\":  resource.MustParse(\"1\"),\n\t\t\t\t\t},\n\t\t\t\t\tLimits: v1.ResourceList{\n\t\t\t\t\t\tv1.ResourceCPU:    resource.MustParse(\"50m\"),\n\t\t\t\t\t\tv1.ResourceMemory: resource.MustParse(\"100Mi\"),\n\t\t\t\t\t\t\"nvidia.com/gpu\":  resource.MustParse(\"2\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmx: &mv1beta1.ContainerMetrics{\n\t\t\t\tName: \"fred\",\n\t\t\t\tUsage: v1.ResourceList{\n\t\t\t\t\tv1.ResourceCPU:    resource.MustParse(\"10m\"),\n\t\t\t\t\tv1.ResourceMemory: resource.MustParse(\"20Mi\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tc: metric{\n\t\t\t\tcpu: 10,\n\t\t\t\tmem: 20971520,\n\t\t\t},\n\t\t\tr: metric{\n\t\t\t\tcpu:  10,\n\t\t\t\tgpu:  1,\n\t\t\t\tmem:  20971520,\n\t\t\t\tlcpu: 50,\n\t\t\t\tlgpu: 2,\n\t\t\t\tlmem: 104857600,\n\t\t\t},\n\t\t},\n\n\t\t\"amd-limits\": {\n\t\t\tcontainer: v1.Container{\n\t\t\t\tName:  \"fred\",\n\t\t\t\tImage: \"img\",\n\t\t\t\tResources: v1.ResourceRequirements{\n\t\t\t\t\tLimits: v1.ResourceList{\n\t\t\t\t\t\tv1.ResourceCPU:    resource.MustParse(\"50m\"),\n\t\t\t\t\t\tv1.ResourceMemory: resource.MustParse(\"100Mi\"),\n\t\t\t\t\t\t\"nvidia.com/gpu\":  resource.MustParse(\"2\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmx: &mv1beta1.ContainerMetrics{\n\t\t\t\tName: \"fred\",\n\t\t\t\tUsage: v1.ResourceList{\n\t\t\t\t\tv1.ResourceCPU:    resource.MustParse(\"10m\"),\n\t\t\t\t\tv1.ResourceMemory: resource.MustParse(\"20Mi\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tc: metric{\n\t\t\t\tcpu: 10,\n\t\t\t\tmem: 20971520,\n\t\t\t},\n\t\t\tr: metric{\n\t\t\t\tcpu:  50,\n\t\t\t\tgpu:  2,\n\t\t\t\tmem:  104857600,\n\t\t\t\tlcpu: 50,\n\t\t\t\tlgpu: 2,\n\t\t\t\tlmem: 104857600,\n\t\t\t},\n\t\t},\n\n\t\t\"amd-no-mx\": {\n\t\t\tcontainer: v1.Container{\n\t\t\t\tName:  \"fred\",\n\t\t\t\tImage: \"img\",\n\t\t\t\tResources: v1.ResourceRequirements{\n\t\t\t\t\tRequests: v1.ResourceList{\n\t\t\t\t\t\tv1.ResourceCPU:    resource.MustParse(\"10m\"),\n\t\t\t\t\t\tv1.ResourceMemory: resource.MustParse(\"20Mi\"),\n\t\t\t\t\t\t\"nvidia.com/gpu\":  resource.MustParse(\"1\"),\n\t\t\t\t\t},\n\t\t\t\t\tLimits: v1.ResourceList{\n\t\t\t\t\t\tv1.ResourceCPU:    resource.MustParse(\"50m\"),\n\t\t\t\t\t\tv1.ResourceMemory: resource.MustParse(\"100Mi\"),\n\t\t\t\t\t\t\"nvidia.com/gpu\":  resource.MustParse(\"2\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tr: metric{\n\t\t\t\tcpu:  10,\n\t\t\t\tgpu:  1,\n\t\t\t\tmem:  20971520,\n\t\t\t\tlcpu: 50,\n\t\t\t\tlgpu: 2,\n\t\t\t\tlmem: 104857600,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tc, r := gatherContainerMX(&u.container, u.mx)\n\t\t\tassert.Equal(t, u.c, c)\n\t\t\tassert.Equal(t, u.r, r)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/render/container_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/api/resource\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\tmv1beta1 \"k8s.io/metrics/pkg/apis/metrics/v1beta1\"\n)\n\nfunc TestContainer(t *testing.T) {\n\tvar c render.Container\n\n\tcres := render.ContainerRes{\n\t\tContainer: makeContainer(),\n\t\tStatus:    makeContainerStatus(),\n\t\tMX:        makeContainerMetrics(),\n\t\tAge:       makeAge(),\n\t}\n\tvar r model1.Row\n\trequire.NoError(t, c.Render(cres, \"blee\", &r))\n\tassert.Equal(t, \"fred\", r.ID)\n\tassert.Equal(t, model1.Fields{\n\t\t\"\",\n\t\t\"fred\",\n\t\t\"●\",\n\t\t\"img\",\n\t\t\"false\",\n\t\t\"Running\",\n\t\t\"0\",\n\t\t\"off:off:off\",\n\t\t\"10\",\n\t\t\"20:20\",\n\t\t\"50\",\n\t\t\"50\",\n\t\t\"20\",\n\t\t\"100:100\",\n\t\t\"20\",\n\t\t\"20\",\n\t\t\"0:0\",\n\t\t\"\",\n\t\t\"container is not ready\",\n\t},\n\t\tr.Fields[:len(r.Fields)-1],\n\t)\n}\n\nfunc BenchmarkContainerRender(b *testing.B) {\n\tvar (\n\t\tc    render.Container\n\t\tr    model1.Row\n\t\tcres = render.ContainerRes{\n\t\t\tContainer: makeContainer(),\n\t\t\tStatus:    makeContainerStatus(),\n\t\t\tMX:        makeContainerMetrics(),\n\t\t\tAge:       makeAge(),\n\t\t}\n\t)\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor range b.N {\n\t\t_ = c.Render(cres, \"blee\", &r)\n\t}\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc toQty(s string) resource.Quantity {\n\tq, _ := resource.ParseQuantity(s)\n\treturn q\n}\n\nfunc makeContainerMetrics() *mv1beta1.ContainerMetrics {\n\treturn &mv1beta1.ContainerMetrics{\n\t\tName: \"fred\",\n\t\tUsage: v1.ResourceList{\n\t\t\tv1.ResourceCPU:    toQty(\"10m\"),\n\t\t\tv1.ResourceMemory: toQty(\"20Mi\"),\n\t\t},\n\t}\n}\n\nfunc makeAge() metav1.Time {\n\treturn metav1.Time{Time: testTime()}\n}\n\nfunc makeContainer() *v1.Container {\n\treturn &v1.Container{\n\t\tName:  \"fred\",\n\t\tImage: \"img\",\n\t\tResources: v1.ResourceRequirements{\n\t\t\tLimits: v1.ResourceList{\n\t\t\t\tv1.ResourceCPU:    toQty(\"20m\"),\n\t\t\t\tv1.ResourceMemory: toQty(\"100Mi\"),\n\t\t\t},\n\t\t},\n\t\tEnv: []v1.EnvVar{\n\t\t\t{\n\t\t\t\tName:  \"fred\",\n\t\t\t\tValue: \"1\",\n\t\t\t\tValueFrom: &v1.EnvVarSource{\n\t\t\t\t\tConfigMapKeyRef: &v1.ConfigMapKeySelector{Key: \"blee\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc makeContainerStatus() *v1.ContainerStatus {\n\treturn &v1.ContainerStatus{\n\t\tName:         \"fred\",\n\t\tState:        v1.ContainerState{Running: &v1.ContainerStateRunning{}},\n\t\tRestartCount: 0,\n\t}\n}\n\nfunc testTime() time.Time {\n\tt, err := time.Parse(time.RFC3339, \"2018-12-14T10:36:43.326972-07:00\")\n\tif err != nil {\n\t\tfmt.Println(\"TestTime Failed\", err)\n\t}\n\treturn t\n}\n"
  },
  {
    "path": "internal/render/context.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n\t\"k8s.io/client-go/tools/clientcmd/api\"\n)\n\n// Context renders a K8s ConfigMap to screen.\ntype Context struct {\n\tBase\n}\n\n// ColorerFunc colors a resource row.\nfunc (Context) ColorerFunc() model1.ColorerFunc {\n\treturn func(ns string, h model1.Header, r *model1.RowEvent) tcell.Color {\n\t\tc := model1.DefaultColorer(ns, h, r)\n\t\tif strings.Contains(strings.TrimSpace(r.Row.Fields[0]), \"*\") {\n\t\t\treturn model1.HighlightColor\n\t\t}\n\n\t\treturn c\n\t}\n}\n\n// Header returns a header row.\nfunc (Context) Header(string) model1.Header {\n\treturn model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"NAME\"},\n\t\tmodel1.HeaderColumn{Name: \"CLUSTER\"},\n\t\tmodel1.HeaderColumn{Name: \"AUTHINFO\"},\n\t\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\t}\n}\n\n// Render renders a K8s resource to screen.\nfunc (Context) Render(o any, _ string, r *model1.Row) error {\n\tctx, ok := o.(*NamedContext)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected *NamedContext, but got %T\", o)\n\t}\n\n\tname := ctx.Name\n\tif ctx.IsCurrentContext(ctx.Name) {\n\t\tname += \"(*)\"\n\t}\n\n\tr.ID = ctx.Name\n\tr.Fields = model1.Fields{\n\t\tname,\n\t\tctx.Context.Cluster,\n\t\tctx.Context.AuthInfo,\n\t\tctx.Context.Namespace,\n\t}\n\n\treturn nil\n}\n\n// Helpers...\n\n// NamedContext represents a named cluster context.\ntype NamedContext struct {\n\tName    string\n\tContext *api.Context\n\tConfig  ContextNamer\n}\n\n// ContextNamer represents a named context.\ntype ContextNamer interface {\n\tCurrentContextName() (string, error)\n}\n\n// NewNamedContext returns a new named context.\nfunc NewNamedContext(c ContextNamer, n string, ctx *api.Context) *NamedContext {\n\treturn &NamedContext{Name: n, Context: ctx, Config: c}\n}\n\n// IsCurrentContext return the active context name.\nfunc (c *NamedContext) IsCurrentContext(n string) bool {\n\tcl, err := c.Config.CurrentContextName()\n\tif err != nil {\n\t\tslog.Error(\"Fail to retrieve current context. Exiting!\")\n\t\tos.Exit(1)\n\t}\n\treturn cl == n\n}\n\n// GetObjectKind returns a schema object.\nfunc (*NamedContext) GetObjectKind() schema.ObjectKind {\n\treturn nil\n}\n\n// DeepCopyObject returns a container copy.\nfunc (c *NamedContext) DeepCopyObject() runtime.Object {\n\treturn c\n}\n"
  },
  {
    "path": "internal/render/context_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/client-go/tools/clientcmd/api\"\n)\n\nfunc TestContextHeader(t *testing.T) {\n\tvar c render.Context\n\n\tassert.Len(t, c.Header(\"\"), 4)\n}\n\nfunc TestContextRender(t *testing.T) {\n\tuu := map[string]struct {\n\t\tctx *render.NamedContext\n\t\te   model1.Row\n\t}{\n\t\t\"active\": {\n\t\t\tctx: &render.NamedContext{\n\t\t\t\tName: \"c1\",\n\t\t\t\tContext: &api.Context{\n\t\t\t\t\tLocationOfOrigin: \"fred\",\n\t\t\t\t\tCluster:          \"c1\",\n\t\t\t\t\tAuthInfo:         \"u1\",\n\t\t\t\t\tNamespace:        \"ns1\",\n\t\t\t\t},\n\t\t\t\tConfig: &config{},\n\t\t\t},\n\t\t\te: model1.Row{\n\t\t\t\tID:     \"c1\",\n\t\t\t\tFields: model1.Fields{\"c1\", \"c1\", \"u1\", \"ns1\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tvar r render.Context\n\tfor k := range uu {\n\t\tuc := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\trow := model1.NewRow(4)\n\t\t\terr := r.Render(uc.ctx, \"\", &row)\n\n\t\t\trequire.NoError(t, err)\n\t\t\tassert.Equal(t, uc.e, row)\n\t\t})\n\t}\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\ntype config struct{}\n\nfunc (config) CurrentContextName() (string, error) {\n\treturn \"fred\", nil\n}\n"
  },
  {
    "path": "internal/render/cr.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\trbacv1 \"k8s.io/api/rbac/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\n// ClusterRole renders a K8s ClusterRole to screen.\ntype ClusterRole struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (c ClusterRole) Header(_ string) model1.Header {\n\treturn c.doHeader(defaultCRHeader)\n}\n\n// Header returns a header rbw.\nvar defaultCRHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"LABELS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// Render renders a K8s resource to screen.\nfunc (p ClusterRole) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting Unstructured, but got %T\", o)\n\t}\n\tif err := p.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif p.specs.isEmpty() {\n\t\treturn nil\n\t}\n\tcols, err := p.specs.realize(raw, defaultCRHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\n// Render renders a K8s resource to screen.\nfunc (ClusterRole) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {\n\tvar cr rbacv1.ClusterRole\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &cr)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tr.ID = client.FQN(\"-\", cr.Name)\n\tr.Fields = model1.Fields{\n\t\tcr.Name,\n\t\tmapToStr(cr.Labels),\n\t\tToAge(cr.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/render/cr_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestClusterRoleRender(t *testing.T) {\n\tc := render.ClusterRole{}\n\tr := model1.NewRow(2)\n\n\trequire.NoError(t, c.Render(load(t, \"cr\"), \"-\", &r))\n\tassert.Equal(t, \"-/blee\", r.ID)\n\tassert.Equal(t, model1.Fields{\"blee\"}, r.Fields[:1])\n}\n"
  },
  {
    "path": "internal/render/crb.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\trbacv1 \"k8s.io/api/rbac/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar defaultCRBHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"CLUSTERROLE\"},\n\tmodel1.HeaderColumn{Name: \"SUBJECT-KIND\"},\n\tmodel1.HeaderColumn{Name: \"SUBJECTS\"},\n\tmodel1.HeaderColumn{Name: \"LABELS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// ClusterRoleBinding renders a K8s ClusterRoleBinding to screen.\ntype ClusterRoleBinding struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (c ClusterRoleBinding) Header(_ string) model1.Header {\n\treturn c.doHeader(defaultCRBHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (c ClusterRoleBinding) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tif err := c.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif c.specs.isEmpty() {\n\t\treturn nil\n\t}\n\n\tcols, err := c.specs.realize(raw, defaultCRBHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\nfunc (ClusterRoleBinding) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {\n\tvar crb rbacv1.ClusterRoleBinding\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &crb)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tkind, ss := renderSubjects(crb.Subjects)\n\n\tr.ID = client.FQN(\"-\", crb.Name)\n\tr.Fields = model1.Fields{\n\t\tcrb.Name,\n\t\tcrb.RoleRef.Name,\n\t\tkind,\n\t\tss,\n\t\tmapToStr(crb.Labels),\n\t\tToAge(crb.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/render/crb_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestClusterRoleBindingRender(t *testing.T) {\n\tc := render.ClusterRoleBinding{}\n\tr := model1.NewRow(5)\n\n\trequire.NoError(t, c.Render(load(t, \"crb\"), \"-\", &r))\n\tassert.Equal(t, \"-/blee\", r.ID)\n\tassert.Equal(t, model1.Fields{\"blee\", \"blee\", \"User\", \"fernand\"}, r.Fields[:4])\n}\n"
  },
  {
    "path": "internal/render/crd.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\tv1 \"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar defaultCRDHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"GROUP\"},\n\tmodel1.HeaderColumn{Name: \"KIND\"},\n\tmodel1.HeaderColumn{Name: \"VERSIONS\"},\n\tmodel1.HeaderColumn{Name: \"SCOPE\"},\n\tmodel1.HeaderColumn{Name: \"ALIASES\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"LABELS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// CustomResourceDefinition renders a K8s CustomResourceDefinition to screen.\ntype CustomResourceDefinition struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (c CustomResourceDefinition) Header(_ string) model1.Header {\n\treturn c.doHeader(defaultCRDHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (c CustomResourceDefinition) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\n\tif err := c.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif c.specs.isEmpty() {\n\t\treturn nil\n\t}\n\tcols, err := c.specs.realize(raw, defaultCRDHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\n// Render renders a K8s resource to screen.\nfunc (c CustomResourceDefinition) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {\n\tvar crd v1.CustomResourceDefinition\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &crd)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tversions := make([]string, 0, len(crd.Spec.Versions))\n\tfor _, v := range crd.Spec.Versions {\n\t\tif v.Served {\n\t\t\tn := v.Name\n\t\t\tif v.Deprecated {\n\t\t\t\tn += \"!\"\n\t\t\t}\n\t\t\tversions = append(versions, n)\n\t\t}\n\t}\n\tif len(versions) == 0 {\n\t\tslog.Warn(\"Unable to assert CRD versions\", slogs.FQN, crd.Name)\n\t}\n\n\tr.ID = client.MetaFQN(&crd.ObjectMeta)\n\tr.Fields = model1.Fields{\n\t\tcrd.Spec.Names.Plural,\n\t\tcrd.Spec.Group,\n\t\tcrd.Spec.Names.Kind,\n\t\tnaStrings(versions),\n\t\tstring(crd.Spec.Scope),\n\t\tnaStrings(crd.Spec.Names.ShortNames),\n\t\tmapToIfc(crd.GetLabels()),\n\t\tAsStatus(c.diagnose(crd.Name, crd.Spec.Versions)),\n\t\tToAge(crd.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n\nfunc (CustomResourceDefinition) diagnose(n string, vv []v1.CustomResourceDefinitionVersion) error {\n\tif len(vv) == 0 {\n\t\treturn fmt.Errorf(\"unable to assert CRD servers versions for %s\", n)\n\t}\n\n\tvar (\n\t\tee     []error\n\t\tserved bool\n\t)\n\tfor _, v := range vv {\n\t\tif v.Served {\n\t\t\tserved = true\n\t\t}\n\t\tif v.Deprecated {\n\t\t\tif v.DeprecationWarning != nil {\n\t\t\t\tee = append(ee, fmt.Errorf(\"%s\", *v.DeprecationWarning))\n\t\t\t} else {\n\t\t\t\tee = append(ee, fmt.Errorf(\"%s[%s] is deprecated\", n, v.Name))\n\t\t\t}\n\t\t}\n\t}\n\tif !served {\n\t\tee = append(ee, fmt.Errorf(\"CRD %s is no longer served by the api server\", n))\n\t}\n\n\tif len(ee) == 0 {\n\t\treturn nil\n\t}\n\terrs := make([]string, 0, len(ee))\n\tfor _, e := range ee {\n\t\terrs = append(errs, e.Error())\n\t}\n\n\treturn errors.New(strings.Join(errs, \" - \"))\n}\n"
  },
  {
    "path": "internal/render/crd_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCustomResourceDefinitionRender(t *testing.T) {\n\tc := render.CustomResourceDefinition{}\n\tr := model1.NewRow(2)\n\n\trequire.NoError(t, c.Render(load(t, \"crd\"), \"\", &r))\n\tassert.Equal(t, \"-/adapters.config.istio.io\", r.ID)\n\tassert.Equal(t, \"adapters\", r.Fields[0])\n\tassert.Equal(t, \"config.istio.io\", r.Fields[1])\n}\n"
  },
  {
    "path": "internal/render/cronjob.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\tbatchv1 \"k8s.io/api/batch/v1\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar defaultCJHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"VS\", Attrs: model1.Attrs{VS: true}},\n\tmodel1.HeaderColumn{Name: \"SCHEDULE\"},\n\tmodel1.HeaderColumn{Name: \"SUSPEND\"},\n\tmodel1.HeaderColumn{Name: \"ACTIVE\"},\n\tmodel1.HeaderColumn{Name: \"LAST_SCHEDULE\", Attrs: model1.Attrs{Time: true}},\n\tmodel1.HeaderColumn{Name: \"SELECTOR\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"CONTAINERS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"IMAGES\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"LABELS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// CronJob renders a K8s CronJob to screen.\ntype CronJob struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (c CronJob) Header(_ string) model1.Header {\n\treturn c.doHeader(defaultCJHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (c CronJob) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tif err := c.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif c.specs.isEmpty() {\n\t\treturn nil\n\t}\n\n\tcols, err := c.specs.realize(raw, defaultCJHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\n// Render renders a K8s resource to screen.\nfunc (CronJob) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {\n\tvar cj batchv1.CronJob\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &cj)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tlastScheduled := \"<none>\"\n\tif cj.Status.LastScheduleTime != nil {\n\t\tlastScheduled = ToAge(*cj.Status.LastScheduleTime)\n\t}\n\n\tr.ID = client.MetaFQN(&cj.ObjectMeta)\n\tr.Fields = model1.Fields{\n\t\tcj.Namespace,\n\t\tcj.Name,\n\t\tcomputeVulScore(cj.Namespace, cj.Labels, &cj.Spec.JobTemplate.Spec.Template.Spec),\n\t\tcj.Spec.Schedule,\n\t\tboolPtrToStr(cj.Spec.Suspend),\n\t\tstrconv.Itoa(len(cj.Status.Active)),\n\t\tlastScheduled,\n\t\tjobSelector(&cj.Spec.JobTemplate.Spec),\n\t\tpodContainerNames(&cj.Spec.JobTemplate.Spec.Template.Spec, true),\n\t\tpodImageNames(&cj.Spec.JobTemplate.Spec.Template.Spec, true),\n\t\tmapToStr(cj.Labels),\n\t\t\"\",\n\t\tToAge(cj.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n\n// Helpers\n\nfunc jobSelector(spec *batchv1.JobSpec) string {\n\tif spec.Selector == nil {\n\t\treturn MissingValue\n\t}\n\tif len(spec.Selector.MatchLabels) > 0 {\n\t\treturn mapToStr(spec.Selector.MatchLabels)\n\t}\n\tif len(spec.Selector.MatchExpressions) == 0 {\n\t\treturn \"\"\n\t}\n\n\tss := make([]string, 0, len(spec.Selector.MatchExpressions))\n\tfor _, e := range spec.Selector.MatchExpressions {\n\t\tss = append(ss, e.String())\n\t}\n\n\treturn strings.Join(ss, \" \")\n}\n\nfunc podContainerNames(spec *v1.PodSpec, includeInit bool) string {\n\tcc := make([]string, 0, len(spec.Containers)+len(spec.InitContainers))\n\n\tif includeInit {\n\t\tfor i := range spec.InitContainers {\n\t\t\tcc = append(cc, spec.InitContainers[i].Name)\n\t\t}\n\t}\n\tfor i := range spec.Containers {\n\t\tcc = append(cc, spec.Containers[i].Name)\n\t}\n\n\treturn strings.Join(cc, \",\")\n}\n\nfunc podImageNames(spec *v1.PodSpec, includeInit bool) string {\n\tcc := make([]string, 0, len(spec.Containers)+len(spec.InitContainers))\n\n\tif includeInit {\n\t\tfor i := range spec.InitContainers {\n\t\t\tcc = append(cc, spec.InitContainers[i].Image)\n\t\t}\n\t}\n\tfor i := range spec.Containers {\n\t\tcc = append(cc, spec.Containers[i].Image)\n\t}\n\n\treturn strings.Join(cc, \",\")\n}\n"
  },
  {
    "path": "internal/render/cronjob_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCronJobRender(t *testing.T) {\n\tc := render.CronJob{}\n\tr := model1.NewRow(6)\n\n\trequire.NoError(t, c.Render(load(t, \"cj\"), \"\", &r))\n\tassert.Equal(t, \"default/hello\", r.ID)\n\tassert.Equal(t, model1.Fields{\"default\", \"hello\", \"n/a\", \"*/1 * * * *\", \"false\", \"0\"}, r.Fields[:6])\n}\n"
  },
  {
    "path": "internal/render/cust_col.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"regexp\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/tview\"\n\t\"k8s.io/kubectl/pkg/cmd/get\"\n)\n\nvar fullRX = regexp.MustCompile(`^([\\w\\s%/-]+):?([\\w\\W]*?)\\|?([NTWSLRH]{0,3})$`)\n\ntype colAttr byte\n\nconst (\n\tnumber     colAttr = 'N'\n\tage        colAttr = 'T'\n\twide       colAttr = 'W'\n\tshow       colAttr = 'S'\n\talignLeft  colAttr = 'L'\n\talignRight colAttr = 'R'\n\thide       colAttr = 'H'\n)\n\ntype colAttrs struct {\n\talign    int\n\tmx       bool\n\tmxc      bool\n\tmxm      bool\n\ttime     bool\n\twide     bool\n\tshow     bool\n\thide     bool\n\tcapacity bool\n}\n\nfunc newColFlags(flags string) colAttrs {\n\tc := colAttrs{\n\t\talign: tview.AlignLeft,\n\t\twide:  false,\n\t}\n\tfor _, b := range []byte(flags) {\n\t\tswitch colAttr(b) {\n\t\tcase hide:\n\t\t\tc.hide = true\n\t\tcase wide:\n\t\t\tc.wide, c.show = true, false\n\t\tcase show:\n\t\t\tc.show, c.wide = true, false\n\t\tcase alignLeft:\n\t\t\tc.align = tview.AlignLeft\n\t\tcase alignRight:\n\t\t\tc.align = tview.AlignRight\n\t\tcase age:\n\t\t\tc.time = true\n\t\tcase number:\n\t\t\tc.capacity, c.align = true, tview.AlignRight\n\t\tdefault:\n\t\t\tslog.Warn(\"Unknown column attribute\", slogs.Attr, b)\n\t\t}\n\t}\n\n\treturn c\n}\n\ntype colDef struct {\n\tcolAttrs\n\n\tname string\n\tidx  int\n\tspec string\n}\n\nfunc parse(s string) (colDef, error) {\n\tmm := fullRX.FindStringSubmatch(s)\n\tif len(mm) == 4 {\n\t\tspec, err := get.RelaxedJSONPathExpression(mm[2])\n\t\tif err != nil {\n\t\t\treturn colDef{idx: -1}, err\n\t\t}\n\t\treturn colDef{\n\t\t\tname:     mm[1],\n\t\t\tidx:      -1,\n\t\t\tspec:     spec,\n\t\t\tcolAttrs: newColFlags(mm[3]),\n\t\t}, nil\n\t}\n\n\treturn colDef{idx: -1}, fmt.Errorf(\"invalid column definition %q\", s)\n}\n\nfunc (c colDef) toHeaderCol() model1.HeaderColumn {\n\treturn model1.HeaderColumn{\n\t\tName: c.name,\n\t\tAttrs: model1.Attrs{\n\t\t\tAlign:    c.align,\n\t\t\tWide:     c.wide,\n\t\t\tShow:     c.show,\n\t\t\tTime:     c.time,\n\t\t\tMX:       c.mx,\n\t\t\tMXC:      c.mxc,\n\t\t\tMXM:      c.mxm,\n\t\t\tHide:     c.hide,\n\t\t\tCapacity: c.capacity,\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/render/cust_col_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/derailed/tview\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCustCol_parse(t *testing.T) {\n\tuu := map[string]struct {\n\t\ts   string\n\t\terr error\n\t\te   colDef\n\t}{\n\t\t\"empty\": {\n\t\t\terr: errors.New(`invalid column definition \"\"`),\n\t\t},\n\n\t\t\"plain\": {\n\t\t\ts: \"fred\",\n\t\t\te: colDef{\n\t\t\t\tname: \"fred\",\n\t\t\t\tidx:  -1,\n\t\t\t\tcolAttrs: colAttrs{\n\t\t\t\t\talign: tview.AlignLeft,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"plain-wide\": {\n\t\t\ts: \"fred|W\",\n\t\t\te: colDef{\n\t\t\t\tname: \"fred\",\n\t\t\t\tidx:  -1,\n\t\t\t\tcolAttrs: colAttrs{\n\t\t\t\t\talign: tview.AlignLeft,\n\t\t\t\t\twide:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"plain-hide\": {\n\t\t\ts: \"fred|WH\",\n\t\t\te: colDef{\n\t\t\t\tname: \"fred\",\n\t\t\t\tidx:  -1,\n\t\t\t\tcolAttrs: colAttrs{\n\t\t\t\t\talign: tview.AlignLeft,\n\t\t\t\t\twide:  true,\n\t\t\t\t\thide:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"plain-show\": {\n\t\t\ts: \"fred|S\",\n\t\t\te: colDef{\n\t\t\t\tname: \"fred\",\n\t\t\t\tidx:  -1,\n\t\t\t\tcolAttrs: colAttrs{\n\t\t\t\t\talign: tview.AlignLeft,\n\t\t\t\t\tshow:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"age\": {\n\t\t\ts: \"AGE|TR\",\n\t\t\te: colDef{\n\t\t\t\tname: \"AGE\",\n\t\t\t\tidx:  -1,\n\t\t\t\tcolAttrs: colAttrs{\n\t\t\t\t\talign: tview.AlignRight,\n\t\t\t\t\ttime:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"plain-wide-right\": {\n\t\t\ts: \"fred|WR\",\n\t\t\te: colDef{\n\t\t\t\tname: \"fred\",\n\t\t\t\tidx:  -1,\n\t\t\t\tcolAttrs: colAttrs{\n\t\t\t\t\talign: tview.AlignRight,\n\t\t\t\t\twide:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"complex\": {\n\t\t\ts: \"BLEE:.spec.addresses[?(@.type == 'CiliumInternalIP')].ip\",\n\t\t\te: colDef{\n\t\t\t\tname: \"BLEE\",\n\t\t\t\tidx:  -1,\n\t\t\t\tspec: \"{.spec.addresses[?(@.type == 'CiliumInternalIP')].ip}\",\n\t\t\t\tcolAttrs: colAttrs{\n\t\t\t\t\talign: tview.AlignLeft,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"complex-wide\": {\n\t\t\ts: \"BLEE:.spec.addresses[?(@.type == 'CiliumInternalIP')].ip|WR\",\n\t\t\te: colDef{\n\t\t\t\tname: \"BLEE\",\n\t\t\t\tidx:  -1,\n\t\t\t\tspec: \"{.spec.addresses[?(@.type == 'CiliumInternalIP')].ip}\",\n\t\t\t\tcolAttrs: colAttrs{\n\t\t\t\t\talign: tview.AlignRight,\n\t\t\t\t\twide:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"full-complex-wide\": {\n\t\t\ts: \"BLEE:.spec.addresses[?(@.type == 'CiliumInternalIP')].ip|WR\",\n\t\t\te: colDef{\n\t\t\t\tname: \"BLEE\",\n\t\t\t\tidx:  -1,\n\t\t\t\tspec: \"{.spec.addresses[?(@.type == 'CiliumInternalIP')].ip}\",\n\t\t\t\tcolAttrs: colAttrs{\n\t\t\t\t\talign: tview.AlignRight,\n\t\t\t\t\twide:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"full-number-wide\": {\n\t\t\ts: \"fred:.metadata.name|NW\",\n\t\t\te: colDef{\n\t\t\t\tname: \"fred\",\n\t\t\t\tidx:  -1,\n\t\t\t\tspec: \"{.metadata.name}\",\n\t\t\t\tcolAttrs: colAttrs{\n\t\t\t\t\talign:    tview.AlignRight,\n\t\t\t\t\tcapacity: true,\n\t\t\t\t\twide:     true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"full-wide\": {\n\t\t\ts: \"fred:.metadata.name|RW\",\n\t\t\te: colDef{\n\t\t\t\tname: \"fred\",\n\t\t\t\tidx:  -1,\n\t\t\t\tspec: \"{.metadata.name}\",\n\t\t\t\tcolAttrs: colAttrs{\n\t\t\t\t\talign: tview.AlignRight,\n\t\t\t\t\twide:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"partial-time-no-wide\": {\n\t\t\ts: \"fred:.metadata.name|T\",\n\t\t\te: colDef{\n\t\t\t\tname: \"fred\",\n\t\t\t\tidx:  -1,\n\t\t\t\tspec: \"{.metadata.name}\",\n\t\t\t\tcolAttrs: colAttrs{\n\t\t\t\t\talign: tview.AlignLeft,\n\t\t\t\t\ttime:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"partial-no-type-no-wide\": {\n\t\t\ts: \"fred:.metadata.name\",\n\t\t\te: colDef{\n\t\t\t\tname: \"fred\",\n\t\t\t\tidx:  -1,\n\t\t\t\tspec: \"{.metadata.name}\",\n\t\t\t\tcolAttrs: colAttrs{\n\t\t\t\t\talign: tview.AlignLeft,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"partial-no-type-wide\": {\n\t\t\ts: \"fred:.metadata.name|W\",\n\t\t\te: colDef{\n\t\t\t\tname: \"fred\",\n\t\t\t\tidx:  -1,\n\t\t\t\tspec: \"{.metadata.name}\",\n\t\t\t\tcolAttrs: colAttrs{\n\t\t\t\t\talign: tview.AlignLeft,\n\t\t\t\t\twide:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"toast\": {\n\t\t\ts: \"fred||.metadata.name|W\",\n\t\t\te: colDef{\n\t\t\t\tname: \"fred\",\n\t\t\t\tidx:  -1,\n\t\t\t\tspec: \"{.||.metadata.name}\",\n\t\t\t\tcolAttrs: colAttrs{\n\t\t\t\t\talign: tview.AlignLeft,\n\t\t\t\t\twide:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"toast-no-name\": {\n\t\t\ts:   \":.metadata.name.fred|TW\",\n\t\t\terr: errors.New(`invalid column definition \":.metadata.name.fred|TW\"`),\n\t\t},\n\n\t\t\"spec-column-typed\": {\n\t\t\ts: `fred:.metadata.name.k8s:fred\\.blee|TW`,\n\t\t\te: colDef{\n\t\t\t\tname: \"fred\",\n\t\t\t\tspec: `{.metadata.name.k8s:fred\\.blee}`,\n\t\t\t\tidx:  -1,\n\t\t\t\tcolAttrs: colAttrs{\n\t\t\t\t\talign: tview.AlignLeft,\n\t\t\t\t\ttime:  true,\n\t\t\t\t\twide:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"partial-no-spec-no-wide\": {\n\t\t\ts: \"fred|T\",\n\t\t\te: colDef{\n\t\t\t\tname: \"fred\",\n\t\t\t\tidx:  -1,\n\t\t\t\tcolAttrs: colAttrs{\n\t\t\t\t\talign: tview.AlignLeft,\n\t\t\t\t\ttime:  true,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tc, err := parse(u.s)\n\t\t\tif err != nil {\n\t\t\t\tassert.Equal(t, u.err, err)\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, u.e, c)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/render/cust_cols.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"reflect\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/itchyny/gojq\"\n\t\"k8s.io/apimachinery/pkg/api/resource\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/client-go/util/jsonpath\"\n)\n\n// ColsSpecs represents a collection of column specification ie NAME:spec|flags.\ntype ColsSpecs []string\n\n// NewColsSpecs returns a new instance.\nfunc NewColsSpecs(cols ...string) ColsSpecs {\n\treturn ColsSpecs(cols)\n}\n\nfunc (cc ColsSpecs) parseSpecs() (ColumnSpecs, error) {\n\tspecs := make(ColumnSpecs, 0, len(cc))\n\n\tfor _, c := range cc {\n\t\tdef, err := parse(c)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tspecs = append(specs, ColumnSpec{\n\t\t\tHeader: def.toHeaderCol(),\n\t\t\tSpec:   def.spec,\n\t\t})\n\t}\n\n\treturn specs, nil\n}\n\n// RenderedCols tracks a collection of column header and cust column parse expression.\ntype RenderedCols []RenderedCol\n\nfunc (rr RenderedCols) hydrateRow(row *model1.Row) {\n\tff := make(model1.Fields, 0, len(row.Fields))\n\tfor _, c := range rr {\n\t\tff = append(ff, c.Value)\n\t}\n\trow.Fields = ff\n}\n\n// HasHeader checks if a given header is present in the collection.\nfunc (rr RenderedCols) HasHeader(n string) bool {\n\tfor _, r := range rr {\n\t\tif r.has(n) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// RenderedCol represents a column header and a column spec.\ntype RenderedCol struct {\n\tHeader model1.HeaderColumn\n\tValue  string\n}\n\n// Has checks if the header column match the given name.\nfunc (r RenderedCol) has(n string) bool {\n\treturn r.Header.Name == n\n}\n\n// ColumnSpec tracks a header column and an options cust column spec.\ntype ColumnSpec struct {\n\tHeader model1.HeaderColumn\n\tSpec   string\n}\n\n// ColumnSpecs tracks a collection of column specs.\ntype ColumnSpecs []ColumnSpec\n\nfunc (c ColumnSpecs) isEmpty() bool {\n\treturn len(c) == 0\n}\n\n// Header builds a new header that is a super set of custom and/or default header.\nfunc (cc ColumnSpecs) Header(rh model1.Header) model1.Header {\n\thh := make(model1.Header, 0, len(cc))\n\tfor _, h := range cc {\n\t\thh = append(hh, h.Header)\n\t}\n\n\tfor _, h := range rh {\n\t\tif idx, ok := hh.IndexOf(h.Name, true); ok {\n\t\t\thh[idx].Attrs = hh[idx].Merge(h.Attrs)\n\t\t\tcontinue\n\t\t}\n\t\thh = append(hh, h)\n\t}\n\n\treturn hh\n}\n\nfunc (cc ColumnSpecs) realize(o runtime.Object, rh model1.Header, row *model1.Row) (RenderedCols, error) {\n\tparsers := make([]*jsonpath.JSONPath, len(cc))\n\tfor ix := range cc {\n\t\tif cc[ix].Spec == \"\" {\n\t\t\tparsers[ix] = nil\n\t\t\tcontinue\n\t\t}\n\t\tparsers[ix] = jsonpath.New(\n\t\t\tfmt.Sprintf(\"column%d\", ix),\n\t\t).AllowMissingKeys(true)\n\t\tif err := parsers[ix].Parse(cc[ix].Spec); err != nil && !isJQSpec(cc[ix].Spec) {\n\t\t\tslog.Warn(\"Unable to parse custom column\",\n\t\t\t\tslogs.Name, cc[ix].Header.Name,\n\t\t\t\tslogs.Error, err,\n\t\t\t)\n\t\t}\n\t}\n\n\tvv, err := hydrate(o, cc, parsers, rh, row)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfor _, hc := range rh {\n\t\tif vv.HasHeader(hc.Name) {\n\t\t\tcontinue\n\t\t}\n\t\tif idx, ok := rh.IndexOf(hc.Name, true); ok {\n\t\t\trc := RenderedCol{Header: hc, Value: row.Fields[idx]}\n\t\t\trc.Header.Wide = true\n\t\t\tvv = append(vv, rc)\n\t\t}\n\t}\n\n\treturn vv, nil\n}\n\nfunc hydrate(o runtime.Object, cc ColumnSpecs, parsers []*jsonpath.JSONPath, rh model1.Header, row *model1.Row) (RenderedCols, error) {\n\tcols := make(RenderedCols, len(parsers))\n\tfor idx := range parsers {\n\t\tparser := parsers[idx]\n\t\tif parser == nil {\n\t\t\tix, ok := rh.IndexOf(cc[idx].Header.Name, true)\n\t\t\tif !ok {\n\t\t\t\tcols[idx] = RenderedCol{\n\t\t\t\t\tHeader: cc[idx].Header,\n\t\t\t\t\tValue:  NAValue,\n\t\t\t\t}\n\t\t\t\tslog.Warn(\"Unable to find custom column\", slogs.Name, cc[idx].Header.Name)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tvar v string\n\t\t\tif ix >= len(row.Fields) {\n\t\t\t\tv = NAValue\n\t\t\t} else {\n\t\t\t\tv = row.Fields[ix]\n\t\t\t}\n\t\t\tcols[idx] = RenderedCol{\n\t\t\t\tHeader: rh[ix],\n\t\t\t\tValue:  v,\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif o == nil {\n\t\t\tcols[idx] = RenderedCol{\n\t\t\t\tHeader: cc[idx].Header,\n\t\t\t\tValue:  NAValue,\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tvar (\n\t\t\tvals [][]reflect.Value\n\t\t\terr  error\n\t\t)\n\t\tif unstructured, ok := o.(runtime.Unstructured); ok {\n\t\t\tif vals, ok := jqParse(cc[idx].Spec, unstructured.UnstructuredContent()); ok {\n\t\t\t\tcols[idx] = RenderedCol{\n\t\t\t\t\tHeader: cc[idx].Header,\n\t\t\t\t\tValue:  vals,\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tvals, err = parser.FindResults(unstructured.UnstructuredContent())\n\t\t} else {\n\t\t\trv := reflect.ValueOf(o)\n\t\t\tif !rv.IsValid() || (rv.Kind() == reflect.Ptr && rv.IsNil()) {\n\t\t\t\tcols[idx] = RenderedCol{\n\t\t\t\t\tHeader: cc[idx].Header,\n\t\t\t\t\tValue:  NAValue,\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tvals, err = parser.FindResults(rv.Elem().Interface())\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tvalues := make([]string, 0, len(vals))\n\t\tif len(vals) == 0 || len(vals[0]) == 0 {\n\t\t\tvalues = append(values, MissingValue)\n\t\t}\n\t\tfor i := range vals {\n\t\t\tfor j := range vals[i] {\n\t\t\t\tvar (\n\t\t\t\t\tstrVal string\n\t\t\t\t\tv      = vals[i][j].Interface()\n\t\t\t\t)\n\t\t\t\tswitch {\n\t\t\t\tcase cc[idx].Header.MXC:\n\t\t\t\t\tswitch k := v.(type) {\n\t\t\t\t\tcase resource.Quantity:\n\t\t\t\t\t\tstrVal = toMc(k.MilliValue())\n\t\t\t\t\tcase string:\n\t\t\t\t\t\tif q, err := resource.ParseQuantity(k); err == nil {\n\t\t\t\t\t\t\tstrVal = toMc(q.MilliValue())\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\tcase cc[idx].Header.MXM:\n\t\t\t\t\tswitch k := v.(type) {\n\t\t\t\t\tcase resource.Quantity:\n\t\t\t\t\t\tstrVal = toMi(k.MilliValue())\n\t\t\t\t\tcase string:\n\t\t\t\t\t\tif q, err := resource.ParseQuantity(k); err == nil {\n\t\t\t\t\t\t\tstrVal = toMi(q.MilliValue())\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\tcase cc[idx].Header.Time:\n\t\t\t\t\tswitch k := v.(type) {\n\t\t\t\t\tcase string:\n\t\t\t\t\t\tif t, err := time.Parse(time.RFC3339, k); err == nil {\n\t\t\t\t\t\t\tstrVal = ToAge(metav1.Time{Time: t})\n\t\t\t\t\t\t}\n\t\t\t\t\tcase metav1.Time:\n\t\t\t\t\t\tstrVal = ToAge(k)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif strVal == \"\" {\n\t\t\t\t\tstrVal = fmt.Sprintf(\"%v\", v)\n\t\t\t\t}\n\t\t\t\tvalues = append(values, strVal)\n\t\t\t}\n\t\t}\n\t\tcols[idx] = RenderedCol{\n\t\t\tHeader: cc[idx].Header,\n\t\t\tValue:  strings.Join(values, \",\"),\n\t\t}\n\t}\n\n\treturn cols, nil\n}\n\nfunc isJQSpec(spec string) bool {\n\treturn len(strings.Split(spec, \"|\")) > 2\n}\n\nfunc jqParse(spec string, o map[string]any) (string, bool) {\n\tif !isJQSpec(spec) {\n\t\treturn \"\", false\n\t}\n\n\texp := spec[1 : len(spec)-1]\n\tjq, err := gojq.Parse(exp)\n\tif err != nil {\n\t\tslog.Warn(\"Fail to parse JQ expression\", slogs.JQExp, exp, slogs.Error, err)\n\t\treturn \"\", false\n\t}\n\n\trr := make([]string, 0, 10)\n\titer := jq.Run(o)\n\tfor v, ok := iter.Next(); ok; v, ok = iter.Next() {\n\t\tif e, cool := v.(error); cool && e != nil {\n\t\t\tif errors.Is(e, new(gojq.HaltError)) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tslog.Error(\"JQ expression evaluation failed. Check your query\", slogs.Error, e)\n\t\t\tcontinue\n\t\t}\n\t\trr = append(rr, fmt.Sprintf(\"%v\", v))\n\t}\n\tif len(rr) == 0 {\n\t\treturn \"\", false\n\t}\n\n\treturn strings.Join(rr, \",\"), true\n}\n"
  },
  {
    "path": "internal/render/cust_cols_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/tview\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/client-go/util/jsonpath\"\n)\n\nfunc TestParseSpecs(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcols ColsSpecs\n\t\terr  error\n\t\te    ColumnSpecs\n\t}{\n\t\t\"empty\": {\n\t\t\te: ColumnSpecs{},\n\t\t},\n\n\t\t\"plain\": {\n\t\t\tcols: ColsSpecs{\n\t\t\t\t\"a\",\n\t\t\t\t\"b\",\n\t\t\t\t\"c\",\n\t\t\t},\n\t\t\te: ColumnSpecs{\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName: \"a\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName: \"b\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName: \"c\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"with-spec-plain\": {\n\t\t\tcols: ColsSpecs{\n\t\t\t\t\"a\",\n\t\t\t\t\"b:.metadata.name\",\n\t\t\t\t\"c\",\n\t\t\t},\n\t\t\te: ColumnSpecs{\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName: \"a\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName: \"b\",\n\t\t\t\t\t},\n\t\t\t\t\tSpec: \"{.metadata.name}\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName: \"c\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"with-spec-fq\": {\n\t\t\tcols: ColsSpecs{\n\t\t\t\t\"a\",\n\t\t\t\t\"b:.metadata.name|NW\",\n\t\t\t\t\"c\",\n\t\t\t},\n\t\t\te: ColumnSpecs{\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName: \"a\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName: \"b\",\n\t\t\t\t\t\tAttrs: model1.Attrs{\n\t\t\t\t\t\t\tWide:     true,\n\t\t\t\t\t\t\tCapacity: true,\n\t\t\t\t\t\t\tAlign:    tview.AlignRight,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSpec: \"{.metadata.name}\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName: \"c\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"spec-type-no-wide\": {\n\t\t\tcols: ColsSpecs{\n\t\t\t\t\"a\",\n\t\t\t\t\"b:.metadata.name|T\",\n\t\t\t\t\"c\",\n\t\t\t},\n\t\t\te: ColumnSpecs{\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName: \"a\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName: \"b\",\n\t\t\t\t\t\tAttrs: model1.Attrs{\n\t\t\t\t\t\t\tTime: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tSpec: \"{.metadata.name}\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName: \"c\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"plain-wide\": {\n\t\t\tcols: ColsSpecs{\n\t\t\t\t\"a\",\n\t\t\t\t\"b|W\",\n\t\t\t\t\"c\",\n\t\t\t},\n\t\t\te: ColumnSpecs{\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName: \"a\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName:  \"b\",\n\t\t\t\t\t\tAttrs: model1.Attrs{Wide: true},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName: \"c\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"no-spec-kind-wide\": {\n\t\t\tcols: ColsSpecs{\n\t\t\t\t\"a\",\n\t\t\t\t\"b|NW\",\n\t\t\t\t\"c\",\n\t\t\t},\n\t\t\te: ColumnSpecs{\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName: \"a\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName: \"b\",\n\t\t\t\t\t\tAttrs: model1.Attrs{\n\t\t\t\t\t\t\tAlign:    tview.AlignRight,\n\t\t\t\t\t\t\tCapacity: true,\n\t\t\t\t\t\t\tWide:     true,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName: \"c\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"toast-spec\": {\n\t\t\tcols: ColsSpecs{\n\t\t\t\t\"a\",\n\t\t\t\t\"b:{{crap.bozo}}|NW\",\n\t\t\t\t\"c\",\n\t\t\t},\n\t\t\terr: errors.New(`unexpected path string, expected a 'name1.name2' or '.name1.name2' or '{name1.name2}' or '{.name1.name2}'`),\n\t\t},\n\n\t\t\"no-spec\": {\n\t\t\tcols: ColsSpecs{\n\t\t\t\t\"a\",\n\t\t\t\t\"b|NW\",\n\t\t\t\t\"c\",\n\t\t\t},\n\t\t\te: ColumnSpecs{\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName: \"a\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName:  \"b\",\n\t\t\t\t\t\tAttrs: model1.Attrs{Align: tview.AlignRight, Capacity: true, Wide: true},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tHeader: model1.HeaderColumn{\n\t\t\t\t\t\tName: \"c\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tcols, err := u.cols.parseSpecs()\n\t\t\tassert.Equal(t, u.err, err)\n\t\t\tassert.Equal(t, u.e, cols)\n\t\t})\n\t}\n}\n\nfunc TestHydrateNilObject(t *testing.T) {\n\tcc := ColumnSpecs{\n\t\t{\n\t\t\tHeader: model1.HeaderColumn{Name: \"test\"},\n\t\t\tSpec:   \"{.metadata.name}\",\n\t\t},\n\t}\n\n\tparser := jsonpath.New(fmt.Sprintf(\"column%d\", 0)).AllowMissingKeys(true)\n\terr := parser.Parse(\"{.metadata.name}\")\n\trequire.NoError(t, err)\n\n\tparsers := []*jsonpath.JSONPath{parser}\n\trh := model1.Header{\n\t\t{Name: \"test\"},\n\t}\n\trow := &model1.Row{\n\t\tFields: model1.Fields{\"value1\"},\n\t}\n\n\t// Test with nil object - should not panic\n\tcols, err := hydrate(nil, cc, parsers, rh, row)\n\trequire.NoError(t, err)\n\tassert.Len(t, cols, 1)\n\tassert.Equal(t, NAValue, cols[0].Value)\n}\n"
  },
  {
    "path": "internal/render/dir.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n)\n\n// Dir renders a directory entry to screen.\ntype Dir struct{}\n\n// IsGeneric identifies a generic handler.\nfunc (Dir) IsGeneric() bool {\n\treturn false\n}\n\n// Healthy checks if the resource is healthy.\nfunc (Dir) Healthy(context.Context, any) error {\n\treturn nil\n}\n\n// ColorerFunc colors a resource row.\nfunc (Dir) ColorerFunc() model1.ColorerFunc {\n\treturn func(string, model1.Header, *model1.RowEvent) tcell.Color {\n\t\treturn tcell.ColorCadetBlue\n\t}\n}\n\nfunc (Dir) SetViewSetting(*config.ViewSetting) {}\n\n// Header returns a header row.\nfunc (Dir) Header(string) model1.Header {\n\treturn model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"NAME\"},\n\t}\n}\n\n// Render renders a K8s resource to screen.\n// BOZO!! Pass in a row with pre-alloc fields??\nfunc (Dir) Render(o any, _ string, r *model1.Row) error {\n\td, ok := o.(DirRes)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected DirRes, but got %T\", o)\n\t}\n\n\tname := \"🦄 \"\n\tif d.Entry.IsDir() {\n\t\tname = \"📁 \"\n\t}\n\tname += d.Entry.Name()\n\tr.ID, r.Fields = d.Path, append(r.Fields, name)\n\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\n// DirRes represents an alias resource.\ntype DirRes struct {\n\tEntry os.DirEntry\n\tPath  string\n}\n\n// GetObjectKind returns a schema object.\nfunc (DirRes) GetObjectKind() schema.ObjectKind {\n\treturn nil\n}\n\n// DeepCopyObject returns a container copy.\nfunc (d DirRes) DeepCopyObject() runtime.Object {\n\treturn d\n}\n"
  },
  {
    "path": "internal/render/dp.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar defaultDPHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"VS\", Attrs: model1.Attrs{VS: true}},\n\tmodel1.HeaderColumn{Name: \"READY\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"UP-TO-DATE\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"AVAILABLE\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"LABELS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// Deployment renders a K8s Deployment to screen.\ntype Deployment struct {\n\tBase\n}\n\n// ColorerFunc colors a resource row.\nfunc (Deployment) ColorerFunc() model1.ColorerFunc {\n\treturn func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color {\n\t\tc := model1.DefaultColorer(ns, h, re)\n\n\t\tidx, ok := h.IndexOf(\"READY\", true)\n\t\tif !ok {\n\t\t\treturn c\n\t\t}\n\t\tready := strings.TrimSpace(re.Row.Fields[idx])\n\t\ttt := strings.Split(ready, \"/\")\n\t\tif len(tt) == 2 && tt[1] == \"0\" {\n\t\t\treturn model1.PendingColor\n\t\t}\n\n\t\treturn c\n\t}\n}\n\n// Header returns a header row.\nfunc (d Deployment) Header(_ string) model1.Header {\n\treturn d.doHeader(defaultDPHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (d Deployment) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tif err := d.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif d.specs.isEmpty() {\n\t\treturn nil\n\t}\n\tcols, err := d.specs.realize(raw, defaultDPHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\n// Render renders a K8s resource to screen.\nfunc (d Deployment) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {\n\tvar dp appsv1.Deployment\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &dp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar desired int32\n\tif dp.Spec.Replicas != nil {\n\t\tdesired = *dp.Spec.Replicas\n\t}\n\tr.ID = client.MetaFQN(&dp.ObjectMeta)\n\tr.Fields = model1.Fields{\n\t\tdp.Namespace,\n\t\tdp.Name,\n\t\tcomputeVulScore(dp.Namespace, dp.Labels, &dp.Spec.Template.Spec),\n\t\tstrconv.Itoa(int(dp.Status.AvailableReplicas)) + \"/\" + strconv.Itoa(int(desired)),\n\t\tstrconv.Itoa(int(dp.Status.UpdatedReplicas)),\n\t\tstrconv.Itoa(int(dp.Status.AvailableReplicas)),\n\t\tmapToStr(dp.Labels),\n\t\tAsStatus(d.diagnose(dp.Status.Replicas, dp.Status.AvailableReplicas)),\n\t\tToAge(dp.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n\nfunc (Deployment) diagnose(desired, avail int32) error {\n\tif desired != avail {\n\t\treturn fmt.Errorf(\"desiring %d replicas got %d available\", desired, avail)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/render/dp_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDpRender(t *testing.T) {\n\tc := render.Deployment{}\n\tr := model1.NewRow(7)\n\n\trequire.NoError(t, c.Render(load(t, \"dp\"), \"\", &r))\n\tassert.Equal(t, \"icx/icx-db\", r.ID)\n\tassert.Equal(t, model1.Fields{\"icx\", \"icx-db\", \"n/a\", \"1/1\", \"1\", \"1\"}, r.Fields[:6])\n}\n\nfunc BenchmarkDpRender(b *testing.B) {\n\tvar (\n\t\tc = render.Deployment{}\n\t\tr = model1.NewRow(7)\n\t\to = load(b, \"dp\")\n\t)\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\tfor range b.N {\n\t\t_ = c.Render(o, \"\", &r)\n\t}\n}\n"
  },
  {
    "path": "internal/render/ds.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/tview\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar defaultDSHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"VS\", Attrs: model1.Attrs{VS: true}},\n\tmodel1.HeaderColumn{Name: \"DESIRED\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"CURRENT\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"READY\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"UP-TO-DATE\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"AVAILABLE\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"LABELS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// DaemonSet renders a K8s DaemonSet to screen.\ntype DaemonSet struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (d DaemonSet) Header(_ string) model1.Header {\n\treturn d.doHeader(defaultDSHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (d DaemonSet) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tif err := d.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif d.specs.isEmpty() {\n\t\treturn nil\n\t}\n\tcols, err := d.specs.realize(raw, defaultDSHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\n// Render renders a K8s resource to screen.\nfunc (d DaemonSet) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {\n\tvar ds appsv1.DaemonSet\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ds)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tr.ID = client.MetaFQN(&ds.ObjectMeta)\n\tr.Fields = model1.Fields{\n\t\tds.Namespace,\n\t\tds.Name,\n\t\tcomputeVulScore(ds.Namespace, ds.Labels, &ds.Spec.Template.Spec),\n\t\tstrconv.Itoa(int(ds.Status.DesiredNumberScheduled)),\n\t\tstrconv.Itoa(int(ds.Status.CurrentNumberScheduled)),\n\t\tstrconv.Itoa(int(ds.Status.NumberReady)),\n\t\tstrconv.Itoa(int(ds.Status.UpdatedNumberScheduled)),\n\t\tstrconv.Itoa(int(ds.Status.NumberAvailable)),\n\t\tmapToStr(ds.Labels),\n\t\tAsStatus(d.diagnose(ds.Status.DesiredNumberScheduled, ds.Status.NumberReady)),\n\t\tToAge(ds.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n\n// Happy returns true if resource is happy, false otherwise.\nfunc (DaemonSet) diagnose(d, r int32) error {\n\tif d != r {\n\t\treturn fmt.Errorf(\"desiring %d replicas but %d ready\", d, r)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/render/ds_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDaemonSetRender(t *testing.T) {\n\tc := render.DaemonSet{}\n\tr := model1.NewRow(9)\n\n\trequire.NoError(t, c.Render(load(t, \"ds\"), \"\", &r))\n\tassert.Equal(t, \"kube-system/fluentd-gcp-v3.2.0\", r.ID)\n\tassert.Equal(t, model1.Fields{\"kube-system\", \"fluentd-gcp-v3.2.0\", \"n/a\", \"2\", \"2\", \"2\", \"2\", \"2\"}, r.Fields[:8])\n}\n"
  },
  {
    "path": "internal/render/ep.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar defaultEPHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"ENDPOINTS\"},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// Endpoints renders a K8s Endpoints to screen.\ntype Endpoints struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (e Endpoints) Header(_ string) model1.Header {\n\treturn e.doHeader(defaultEPHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (e Endpoints) Render(o any, ns string, row *model1.Row) error {\n\tif err := e.defaultRow(o, ns, row); err != nil {\n\t\treturn err\n\t}\n\tif e.specs.isEmpty() {\n\t\treturn nil\n\t}\n\tcols, err := e.specs.realize(o.(*unstructured.Unstructured), defaultEPHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\nfunc (e Endpoints) defaultRow(o any, ns string, r *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tvar ep v1.Endpoints\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ep)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tr.ID = client.MetaFQN(&ep.ObjectMeta)\n\tr.Fields = make(model1.Fields, 0, len(e.Header(ns)))\n\tr.Fields = model1.Fields{\n\t\tep.Namespace,\n\t\tep.Name,\n\t\tmissing(toEPs(ep.Subsets)),\n\t\tToAge(ep.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc toEPs(ss []v1.EndpointSubset) string {\n\taa := make([]string, 0, len(ss))\n\tfor _, s := range ss {\n\t\tpp := make([]string, len(s.Ports))\n\t\tportsToStrs(s.Ports, pp)\n\t\ta := make([]string, len(s.Addresses))\n\t\tprocessIPs(a, pp, s.Addresses)\n\t\taa = append(aa, strings.Join(a, \",\"))\n\t}\n\treturn strings.Join(aa, \",\")\n}\n\nfunc portsToStrs(pp []v1.EndpointPort, ss []string) {\n\tfor i := range pp {\n\t\tss[i] = strconv.Itoa(int(pp[i].Port))\n\t}\n}\n\nfunc processIPs(aa, pp []string, addrs []v1.EndpointAddress) {\n\tconst maxIPs = 3\n\tvar i int\n\tfor _, a := range addrs {\n\t\tif a.IP == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif len(pp) == 0 {\n\t\t\taa[i], i = a.IP, i+1\n\t\t\tcontinue\n\t\t}\n\t\tif len(pp) > maxIPs {\n\t\t\taa[i], i = a.IP+\":\"+strings.Join(pp[:maxIPs], \",\")+\"...\", i+1\n\t\t} else {\n\t\t\taa[i], i = a.IP+\":\"+strings.Join(pp, \",\"), i+1\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/render/ep_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestEndpointsRender(t *testing.T) {\n\tc := render.Endpoints{}\n\tr := model1.NewRow(4)\n\n\trequire.NoError(t, c.Render(load(t, \"ep\"), \"\", &r))\n\tassert.Equal(t, \"ns-1/blee\", r.ID)\n\tassert.Equal(t, model1.Fields{\"ns-1\", \"blee\", \"10.0.0.67:8080\"}, r.Fields[:3])\n}\n"
  },
  {
    "path": "internal/render/eps.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\tdiscoveryv1 \"k8s.io/api/discovery/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar defaultEPsHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"ADDRESSTYPE\"},\n\tmodel1.HeaderColumn{Name: \"PORTS\"},\n\tmodel1.HeaderColumn{Name: \"ENDPOINTS\"},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// EndpointSlice renders a K8s EndpointSlice to screen.\ntype EndpointSlice struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (e EndpointSlice) Header(_ string) model1.Header {\n\treturn e.doHeader(defaultEPsHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (e EndpointSlice) Render(o any, ns string, row *model1.Row) error {\n\tif err := e.defaultRow(o, ns, row); err != nil {\n\t\treturn err\n\t}\n\tif e.specs.isEmpty() {\n\t\treturn nil\n\t}\n\tcols, err := e.specs.realize(o.(*unstructured.Unstructured), defaultEPsHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\nfunc (e EndpointSlice) defaultRow(o any, ns string, r *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tvar eps discoveryv1.EndpointSlice\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &eps)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tr.ID = client.MetaFQN(&eps.ObjectMeta)\n\tr.Fields = make(model1.Fields, 0, len(e.Header(ns)))\n\tr.Fields = model1.Fields{\n\t\teps.Namespace,\n\t\teps.Name,\n\t\tstring(eps.AddressType),\n\t\ttoPorts(eps.Ports),\n\t\ttoEPss(eps.Endpoints),\n\t\tToAge(eps.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc toEPss(ee []discoveryv1.Endpoint) string {\n\tif len(ee) == 0 {\n\t\treturn UnsetValue\n\t}\n\n\taa := make([]string, 0, len(ee))\n\tfor _, e := range ee {\n\t\taa = append(aa, e.Addresses...)\n\t}\n\n\treturn strings.Join(aa, \",\")\n}\n\nfunc toPorts(ee []discoveryv1.EndpointPort) string {\n\tif len(ee) == 0 {\n\t\treturn UnsetValue\n\t}\n\n\taa := make([]string, 0, len(ee))\n\tfor _, e := range ee {\n\t\tif e.Port != nil {\n\t\t\taa = append(aa, strconv.Itoa(int(*e.Port)))\n\t\t}\n\t}\n\n\treturn strings.Join(aa, \",\")\n}\n"
  },
  {
    "path": "internal/render/eps_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestEndpointSliceRender(t *testing.T) {\n\tc := render.EndpointSlice{}\n\tr := model1.NewRow(4)\n\n\trequire.NoError(t, c.Render(load(t, \"eps\"), \"\", &r))\n\tassert.Equal(t, \"blee/fred\", r.ID)\n\tassert.Equal(t, model1.Fields{\"blee\", \"fred\", \"IPv4\", \"4244\", \"172.20.0.2,172.20.0.3\"}, r.Fields[:5])\n}\n"
  },
  {
    "path": "internal/render/ev.go",
    "content": "// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/derailed/k9s/internal/slogs\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// Event renders a event resource to screen.\ntype Event struct {\n\tTable\n}\n\n// Healthy checks component health.\nfunc (*Event) Healthy(_ context.Context, o any) error {\n\tr, ok := o.(metav1.TableRow)\n\tif !ok {\n\t\tslog.Error(\"Expected TableRow\", slogs.Type, fmt.Sprintf(\"%T\", o))\n\t\treturn nil\n\t}\n\tidx := 2\n\tif idx < len(r.Cells) && r.Cells[idx] != \"Normal\" {\n\t\treturn fmt.Errorf(\"event is not normal: %s\", r.Cells[idx])\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/render/generic.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n)\n\nvar defaultGENHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// Generic renders a K8s generic resource to screen.\ntype Generic struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (m Generic) Header(_ string) model1.Header {\n\treturn m.doHeader(defaultGENHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (m Generic) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tif err := m.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif m.specs.isEmpty() {\n\t\treturn nil\n\t}\n\tcols, err := m.specs.realize(o.(*unstructured.Unstructured), defaultGENHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\n// Render renders a K8s resource to screen.\nfunc (Generic) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {\n\tr.ID = client.FQN(raw.GetNamespace(), raw.GetName())\n\tr.Fields = model1.Fields{\n\t\traw.GetNamespace(),\n\t\traw.GetName(),\n\t\t\"\",\n\t\tToAge(raw.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/render/helm/chart.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage helm\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"helm.sh/helm/v3/pkg/release\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n)\n\n// Chart renders a helm chart to screen.\ntype Chart struct{}\n\n// IsGeneric identifies a generic handler.\nfunc (Chart) IsGeneric() bool {\n\treturn false\n}\n\nfunc (Chart) SetViewSetting(*config.ViewSetting) {}\n\n// ColorerFunc colors a resource row.\nfunc (Chart) ColorerFunc() model1.ColorerFunc {\n\treturn model1.DefaultColorer\n}\n\n// Header returns a header row.\nfunc (Chart) Header(_ string) model1.Header {\n\treturn model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\t\tmodel1.HeaderColumn{Name: \"NAME\"},\n\t\tmodel1.HeaderColumn{Name: \"REVISION\"},\n\t\tmodel1.HeaderColumn{Name: \"STATUS\"},\n\t\tmodel1.HeaderColumn{Name: \"CHART\"},\n\t\tmodel1.HeaderColumn{Name: \"APP VERSION\"},\n\t\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\t\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n\t}\n}\n\n// Render renders a chart to screen.\nfunc (c Chart) Render(o any, _ string, r *model1.Row) error {\n\th, ok := o.(ReleaseRes)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected ReleaseRes, but got %T\", o)\n\t}\n\n\tr.ID = client.FQN(h.Release.Namespace, h.Release.Name)\n\tr.Fields = model1.Fields{\n\t\th.Release.Namespace,\n\t\th.Release.Name,\n\t\tstrconv.Itoa(h.Release.Version),\n\t\th.Release.Info.Status.String(),\n\t\th.Release.Chart.Metadata.Name + \"-\" + h.Release.Chart.Metadata.Version,\n\t\th.Release.Chart.Metadata.AppVersion,\n\t\trender.AsStatus(c.diagnose(h.Release.Info.Status.String())),\n\t\trender.ToAge(metav1.Time{Time: h.Release.Info.LastDeployed.Time}),\n\t}\n\n\treturn nil\n}\n\n// Healthy checks component health.\nfunc (c Chart) Healthy(_ context.Context, o any) error {\n\th, ok := o.(*ReleaseRes)\n\tif !ok {\n\t\tslog.Error(\"Expected *ReleaseRes, but got\", slogs.Type, fmt.Sprintf(\"%T\", o))\n\t}\n\n\treturn c.diagnose(h.Release.Info.Status.String())\n}\n\nfunc (Chart) diagnose(s string) error {\n\tif s != \"deployed\" {\n\t\treturn fmt.Errorf(\"chart is in an invalid state\")\n\t}\n\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\n// ReleaseRes represents a helm chart resource.\ntype ReleaseRes struct {\n\tRelease *release.Release\n}\n\n// GetObjectKind returns a schema object.\nfunc (ReleaseRes) GetObjectKind() schema.ObjectKind {\n\treturn nil\n}\n\n// DeepCopyObject returns a container copy.\nfunc (h ReleaseRes) DeepCopyObject() runtime.Object {\n\treturn h\n}\n"
  },
  {
    "path": "internal/render/helm/history.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage helm\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n)\n\n// History renders a History chart to screen.\ntype History struct{}\n\nfunc (History) SetViewSetting(*config.ViewSetting) {}\n\n// IsGeneric identifies a generic handler.\nfunc (History) IsGeneric() bool {\n\treturn false\n}\n\n// ColorerFunc colors a resource row.\nfunc (History) ColorerFunc() model1.ColorerFunc {\n\treturn model1.DefaultColorer\n}\n\n// Header returns a header row.\nfunc (History) Header(_ string) model1.Header {\n\treturn model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"REVISION\"},\n\t\tmodel1.HeaderColumn{Name: \"STATUS\"},\n\t\tmodel1.HeaderColumn{Name: \"CHART\"},\n\t\tmodel1.HeaderColumn{Name: \"APP VERSION\"},\n\t\tmodel1.HeaderColumn{Name: \"DESCRIPTION\"},\n\t\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\t}\n}\n\n// Render renders a chart to screen.\nfunc (c History) Render(o any, _ string, r *model1.Row) error {\n\th, ok := o.(ReleaseRes)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected HistoryRes, but got %T\", o)\n\t}\n\n\tr.ID = client.FQN(h.Release.Namespace, h.Release.Name)\n\tr.ID += \":\" + strconv.Itoa(h.Release.Version)\n\tr.Fields = model1.Fields{\n\t\tstrconv.Itoa(h.Release.Version),\n\t\th.Release.Info.Status.String(),\n\t\th.Release.Chart.Metadata.Name + \"-\" + h.Release.Chart.Metadata.Version,\n\t\th.Release.Chart.Metadata.AppVersion,\n\t\th.Release.Info.Description,\n\t\trender.AsStatus(c.diagnose(h.Release.Info.Status.String())),\n\t}\n\n\treturn nil\n}\n\n// Healthy checks component health.\nfunc (History) Healthy(context.Context, any) error {\n\treturn nil\n}\n\nfunc (History) diagnose(string) error {\n\treturn nil\n}\n"
  },
  {
    "path": "internal/render/helpers.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/vul\"\n\t\"github.com/derailed/tview\"\n\t\"github.com/mattn/go-runewidth\"\n\t\"golang.org/x/text/language\"\n\t\"golang.org/x/text/message\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/util/duration\"\n)\n\n// ExtractImages returns a collection of container images.\n// !!BOZO!! If this has any legs?? enable scans on other container types.\nfunc ExtractImages(spec *v1.PodSpec) []string {\n\tii := make([]string, 0, len(spec.Containers))\n\tfor i := range spec.Containers {\n\t\tii = append(ii, spec.Containers[i].Image)\n\t}\n\n\treturn ii\n}\n\nfunc computeVulScore(ns string, lbls map[string]string, spec *v1.PodSpec) string {\n\tif vul.ImgScanner == nil || !vul.ImgScanner.IsInitialized() || vul.ImgScanner.ShouldExcludes(ns, lbls) {\n\t\treturn NAValue\n\t}\n\tii := ExtractImages(spec)\n\tvul.ImgScanner.Enqueue(context.Background(), ii...)\n\tsc := vul.ImgScanner.Score(ii...)\n\n\treturn sc\n}\n\nfunc runesToNum(rr []rune) int64 {\n\tvar r int64\n\tvar m int64 = 1\n\tfor i := len(rr) - 1; i >= 0; i-- {\n\t\tv := int64(rr[i] - '0')\n\t\tr += v * m\n\t\tm *= 10\n\t}\n\n\treturn r\n}\n\n// AsThousands prints a number with thousand separator.\nfunc AsThousands(n int64) string {\n\tp := message.NewPrinter(language.English)\n\treturn p.Sprintf(\"%d\", n)\n}\n\n// AsStatus returns error as string.\nfunc AsStatus(err error) string {\n\tif err == nil {\n\t\treturn \"\"\n\t}\n\treturn err.Error()\n}\n\nfunc asSelector(s *metav1.LabelSelector) string {\n\tsel, err := metav1.LabelSelectorAsSelector(s)\n\tif err != nil {\n\t\tslog.Error(\"Selector conversion failed\", slogs.Error, err)\n\t\treturn NAValue\n\t}\n\n\treturn sel.String()\n}\n\n// ToSelector flattens a map selector to a string selector.\nfunc toSelector(m map[string]string) string {\n\ts := make([]string, 0, len(m))\n\tfor k, v := range m {\n\t\ts = append(s, k+\"=\"+v)\n\t}\n\n\treturn strings.Join(s, \",\")\n}\n\n// Blank checks if a collection is empty or all values are blank.\nfunc blank(ss []string) bool {\n\tfor _, s := range ss {\n\t\tif s != \"\" {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\n// Join a slice of strings, skipping blanks.\nfunc join(ss []string, sep string) string {\n\tswitch len(ss) {\n\tcase 0:\n\t\treturn \"\"\n\tcase 1:\n\t\treturn ss[0]\n\t}\n\n\tb := make([]string, 0, len(ss))\n\tfor _, s := range ss {\n\t\tif s != \"\" {\n\t\t\tb = append(b, s)\n\t\t}\n\t}\n\tif len(b) == 0 {\n\t\treturn \"\"\n\t}\n\n\tn := len(sep) * (len(b) - 1)\n\tfor i := range b {\n\t\tn += len(ss[i])\n\t}\n\n\tvar buff strings.Builder\n\tbuff.Grow(n)\n\tbuff.WriteString(b[0])\n\tfor _, s := range b[1:] {\n\t\tbuff.WriteString(sep)\n\t\tbuff.WriteString(s)\n\t}\n\n\treturn buff.String()\n}\n\n// AsPerc prints a number as percentage with parens.\nfunc AsPerc(p string) string {\n\treturn \"(\" + p + \")\"\n}\n\n// PrintPerc prints a number as percentage.\nfunc PrintPerc(p int) string {\n\treturn strconv.Itoa(p) + \"%\"\n}\n\n// IntToStr converts an int to a string.\nfunc IntToStr(p int) string {\n\treturn strconv.Itoa(p)\n}\n\nfunc missing(s string) string {\n\treturn check(s, MissingValue)\n}\n\nfunc naStrings(ss []string) string {\n\tif len(ss) == 0 {\n\t\treturn NAValue\n\t}\n\treturn strings.Join(ss, \",\")\n}\n\nfunc na(s string) string {\n\treturn check(s, NAValue)\n}\n\nfunc check(s, sub string) string {\n\tif s == \"\" {\n\t\treturn sub\n\t}\n\n\treturn s\n}\n\nfunc boolToStr(b bool) string {\n\tswitch b {\n\tcase true:\n\t\treturn \"true\"\n\tdefault:\n\t\treturn \"false\"\n\t}\n}\n\n// ToAge converts time to human duration.\nfunc ToAge(t metav1.Time) string {\n\tif t.IsZero() {\n\t\treturn UnknownValue\n\t}\n\n\treturn duration.HumanDuration(time.Since(t.Time))\n}\n\nfunc toAgeHuman(s string) string {\n\tif s == \"\" {\n\t\treturn UnknownValue\n\t}\n\n\tt, err := time.Parse(time.RFC3339, s)\n\tif err != nil {\n\t\treturn NAValue\n\t}\n\n\treturn duration.HumanDuration(time.Since(t))\n}\n\n// Truncate a string to the given l and suffix ellipsis if needed.\nfunc Truncate(str string, width int) string {\n\treturn runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis))\n}\n\nfunc mapToStr(m map[string]string) string {\n\tif len(m) == 0 {\n\t\treturn \"\"\n\t}\n\n\tkk := make([]string, 0, len(m))\n\tfor k := range m {\n\t\tkk = append(kk, k)\n\t}\n\tsort.Strings(kk)\n\n\tbb := make([]byte, 0, 100)\n\tfor i, k := range kk {\n\t\tbb = append(bb, k+\"=\"+m[k]...)\n\t\tif i < len(kk)-1 {\n\t\t\tbb = append(bb, ',')\n\t\t}\n\t}\n\n\treturn string(bb)\n}\n\nfunc mapToIfc(m any) (s string) {\n\tif m == nil {\n\t\treturn \"\"\n\t}\n\n\tmm, ok := m.(map[string]any)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\tif len(mm) == 0 {\n\t\treturn \"\"\n\t}\n\n\tkk := make([]string, 0, len(mm))\n\tfor k := range mm {\n\t\tkk = append(kk, k)\n\t}\n\tsort.Strings(kk)\n\n\tfor i, k := range kk {\n\t\tstr, ok := mm[k].(string)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\ts += k + \"=\" + str\n\t\tif i < len(kk)-1 {\n\t\t\ts += \" \"\n\t\t}\n\t}\n\n\treturn\n}\n\nfunc toMu(v int64) string {\n\tif v == 0 {\n\t\treturn NAValue\n\t}\n\n\treturn strconv.Itoa(int(v))\n}\n\nfunc toMc(v int64) string {\n\tif v == 0 {\n\t\treturn ZeroValue\n\t}\n\treturn strconv.Itoa(int(v))\n}\n\nfunc toMi(v int64) string {\n\tif v == 0 {\n\t\treturn ZeroValue\n\t}\n\treturn strconv.Itoa(int(client.ToMB(v)))\n}\n\nfunc boolPtrToStr(b *bool) string {\n\tif b == nil {\n\t\treturn \"false\"\n\t}\n\n\treturn boolToStr(*b)\n}\n\nfunc strPtrToStr(s *string) string {\n\tif s == nil {\n\t\treturn \"\"\n\t}\n\treturn *s\n}\n\n// Pad a string up to the given length or truncates if greater than length.\nfunc Pad(s string, width int) string {\n\tif len(s) == width {\n\t\treturn s\n\t}\n\n\tif len(s) > width {\n\t\treturn Truncate(s, width)\n\t}\n\n\treturn s + strings.Repeat(\" \", width-len(s))\n}\n"
  },
  {
    "path": "internal/render/helpers_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\tmetav1beta1 \"k8s.io/apimachinery/pkg/apis/meta/v1beta1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nfunc TestTableGenericHydrate(t *testing.T) {\n\traw := load(t, \"p1\")\n\ttt := metav1beta1.Table{\n\t\tColumnDefinitions: []metav1beta1.TableColumnDefinition{\n\t\t\t{Name: \"c1\"},\n\t\t\t{Name: \"c2\"},\n\t\t},\n\t\tRows: []metav1beta1.TableRow{\n\t\t\t{\n\t\t\t\tCells:  []any{\"fred\", 10},\n\t\t\t\tObject: runtime.RawExtension{Object: raw},\n\t\t\t},\n\t\t\t{\n\t\t\t\tCells:  []any{\"blee\", 20},\n\t\t\t\tObject: runtime.RawExtension{Object: raw},\n\t\t\t},\n\t\t},\n\t}\n\trr := make([]model1.Row, 2)\n\tvar re Table\n\tre.SetTable(\"blee\", &tt)\n\n\trequire.NoError(t, model1.GenericHydrate(\"blee\", &tt, rr, &re))\n\tassert.Len(t, rr, 2)\n\tassert.Len(t, rr[0].Fields, 2)\n}\n\nfunc TestTableHydrate(t *testing.T) {\n\too := []runtime.Object{\n\t\t&PodWithMetrics{Raw: load(t, \"p1\")},\n\t}\n\trr := make([]model1.Row, 1)\n\n\tre := NewPod()\n\trequire.NoError(t, model1.Hydrate(\"blee\", oo, rr, re))\n\tassert.Len(t, rr, 1)\n\tassert.Len(t, rr[0].Fields, 26)\n}\n\nfunc TestToAge(t *testing.T) {\n\tuu := map[string]struct {\n\t\tt time.Time\n\t\te string\n\t}{\n\t\t\"zero\": {\n\t\t\tt: time.Time{},\n\t\t\te: UnknownValue,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tuc := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, uc.e, ToAge(metav1.Time{Time: uc.t}))\n\t\t})\n\t}\n}\n\nfunc TestToAgeHuman(t *testing.T) {\n\tuu := map[string]struct {\n\t\tt, e string\n\t}{\n\t\t\"blank\": {\n\t\t\tt: \"\",\n\t\t\te: UnknownValue,\n\t\t},\n\t\t\"good\": {\n\t\t\tt: time.Now().Add(-10 * time.Second).Format(time.RFC3339Nano),\n\t\t\te: \"10s\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, toAgeHuman(u.t))\n\t\t})\n\t}\n}\n\nfunc TestJoin(t *testing.T) {\n\tuu := map[string]struct {\n\t\ti []string\n\t\te string\n\t}{\n\t\t\"zero\":      {[]string{}, \"\"},\n\t\t\"std\":       {[]string{\"a\", \"b\", \"c\"}, \"a,b,c\"},\n\t\t\"blank\":     {[]string{\"\", \"\", \"\"}, \"\"},\n\t\t\"sparse\":    {[]string{\"a\", \"\", \"c\"}, \"a,c\"},\n\t\t\"withBlank\": {[]string{\"\", \"a\", \"c\"}, \"a,c\"},\n\t}\n\n\tfor k := range uu {\n\t\tuc := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, uc.e, join(uc.i, \",\"))\n\t\t})\n\t}\n}\n\nfunc TestBoolPtrToStr(t *testing.T) {\n\ttv, fv := true, false\n\n\tuu := []struct {\n\t\tp *bool\n\t\te string\n\t}{\n\t\t{nil, \"false\"},\n\t\t{&tv, \"true\"},\n\t\t{&fv, \"false\"},\n\t}\n\n\tfor _, u := range uu {\n\t\tassert.Equal(t, u.e, boolPtrToStr(u.p))\n\t}\n}\n\nfunc TestNamespaced(t *testing.T) {\n\tuu := []struct {\n\t\tp, ns, n string\n\t}{\n\t\t{\"fred/blee\", \"fred\", \"blee\"},\n\t}\n\n\tfor _, u := range uu {\n\t\tns, n := client.Namespaced(u.p)\n\t\tassert.Equal(t, u.ns, ns)\n\t\tassert.Equal(t, u.n, n)\n\t}\n}\n\nfunc TestMissing(t *testing.T) {\n\tuu := []struct {\n\t\ti, e string\n\t}{\n\t\t{\"fred\", \"fred\"},\n\t\t{\"\", MissingValue},\n\t}\n\n\tfor _, u := range uu {\n\t\tassert.Equal(t, u.e, missing(u.i))\n\t}\n}\n\nfunc TestBoolToStr(t *testing.T) {\n\tuu := []struct {\n\t\ti bool\n\t\te string\n\t}{\n\t\t{true, \"true\"},\n\t\t{false, \"false\"},\n\t}\n\n\tfor _, u := range uu {\n\t\tassert.Equal(t, u.e, boolToStr(u.i))\n\t}\n}\n\nfunc TestNa(t *testing.T) {\n\tuu := []struct {\n\t\ti, e string\n\t}{\n\t\t{\"fred\", \"fred\"},\n\t\t{\"\", NAValue},\n\t}\n\n\tfor _, u := range uu {\n\t\tassert.Equal(t, u.e, na(u.i))\n\t}\n}\n\nfunc TestTruncate(t *testing.T) {\n\tuu := map[string]struct {\n\t\tdata string\n\t\tsize int\n\t\te    string\n\t}{\n\t\t\"same\": {\n\t\t\tdata: \"fred\",\n\t\t\tsize: 4,\n\t\t\te:    \"fred\",\n\t\t},\n\t\t\"small\": {\n\t\t\tdata: \"fred\",\n\t\t\tsize: 10,\n\t\t\te:    \"fred\",\n\t\t},\n\t\t\"larger\": {\n\t\t\tdata: \"fred\",\n\t\t\tsize: 3,\n\t\t\te:    \"fr…\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, Truncate(u.data, u.size))\n\t\t})\n\t}\n}\n\nfunc TestToSelector(t *testing.T) {\n\tuu := map[string]struct {\n\t\tm map[string]string\n\t\te []string\n\t}{\n\t\t\"cool\": {\n\t\t\tmap[string]string{\"app\": \"fred\", \"env\": \"test\"},\n\t\t\t[]string{\"app=fred,env=test\", \"env=test,app=fred\"},\n\t\t},\n\t\t\"empty\": {\n\t\t\tmap[string]string{},\n\t\t\t[]string{\"\"},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tuc := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\ts := toSelector(uc.m)\n\t\t\tvar match bool\n\t\t\tfor _, e := range uc.e {\n\t\t\t\tif e == s {\n\t\t\t\t\tmatch = true\n\t\t\t\t}\n\t\t\t}\n\t\t\tassert.True(t, match)\n\t\t})\n\t}\n}\n\nfunc TestBlank(t *testing.T) {\n\tuu := map[string]struct {\n\t\ta []string\n\t\te bool\n\t}{\n\t\t\"full\": {\n\t\t\ta: []string{\"fred\", \"blee\"},\n\t\t},\n\t\t\"empty\": {\n\t\t\te: true,\n\t\t},\n\t\t\"blank\": {\n\t\t\ta: []string{\"fred\", \"\"},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tuc := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, uc.e, blank(uc.a))\n\t\t})\n\t}\n}\n\nfunc TestMetaFQN(t *testing.T) {\n\tuu := map[string]struct {\n\t\tm metav1.ObjectMeta\n\t\te string\n\t}{\n\t\t\"full\": {metav1.ObjectMeta{Namespace: \"fred\", Name: \"blee\"}, \"fred/blee\"},\n\t\t\"nons\": {metav1.ObjectMeta{Name: \"blee\"}, \"-/blee\"},\n\t}\n\n\tfor k := range uu {\n\t\tuc := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, uc.e, client.MetaFQN(&uc.m))\n\t\t})\n\t}\n}\n\nfunc TestFQN(t *testing.T) {\n\tuu := map[string]struct {\n\t\tns, n string\n\t\te     string\n\t}{\n\t\t\"full\": {ns: \"fred\", n: \"blee\", e: \"fred/blee\"},\n\t\t\"nons\": {n: \"blee\", e: \"blee\"},\n\t}\n\n\tfor k := range uu {\n\t\tuc := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, uc.e, client.FQN(uc.ns, uc.n))\n\t\t})\n\t}\n}\n\nfunc TestMapToStr(t *testing.T) {\n\tuu := []struct {\n\t\ti map[string]string\n\t\te string\n\t}{\n\t\t{map[string]string{\"blee\": \"duh\", \"aa\": \"bb\"}, \"aa=bb,blee=duh\"},\n\t\t{map[string]string{}, \"\"},\n\t}\n\tfor _, u := range uu {\n\t\tassert.Equal(t, u.e, mapToStr(u.i))\n\t}\n}\n\nfunc BenchmarkMapToStr(b *testing.B) {\n\tll := map[string]string{\n\t\t\"blee\": \"duh\",\n\t\t\"aa\":   \"bb\",\n\t}\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor range b.N {\n\t\tmapToStr(ll)\n\t}\n}\n\nfunc TestRunesToNum(t *testing.T) {\n\tuu := map[string]struct {\n\t\trr []rune\n\t\te  int64\n\t}{\n\t\t\"0\": {\n\t\t\trr: []rune(\"\"),\n\t\t\te:  0,\n\t\t},\n\t\t\"100\": {\n\t\t\trr: []rune(\"100\"),\n\t\t\te:  100,\n\t\t},\n\t\t\"64\": {\n\t\t\trr: []rune(\"64\"),\n\t\t\te:  64,\n\t\t},\n\t\t\"52640\": {\n\t\t\trr: []rune(\"52640\"),\n\t\t\te:  52640,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, runesToNum(u.rr))\n\t\t})\n\t}\n}\n\nfunc BenchmarkRunesToNum(b *testing.B) {\n\trr := []rune(\"5465\")\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor range b.N {\n\t\trunesToNum(rr)\n\t}\n}\n\nfunc TestToMc(t *testing.T) {\n\tuu := []struct {\n\t\tv int64\n\t\te string\n\t}{\n\t\t{0, \"0\"},\n\t\t{2, \"2\"},\n\t\t{1_000, \"1000\"},\n\t}\n\n\tfor _, u := range uu {\n\t\tassert.Equal(t, u.e, toMc(u.v))\n\t}\n}\n\nfunc TestToMi(t *testing.T) {\n\tuu := []struct {\n\t\tv int64\n\t\te string\n\t}{\n\t\t{0, \"0\"},\n\t\t{2 * client.MegaByte, \"2\"},\n\t\t{1_000 * client.MegaByte, \"1000\"},\n\t}\n\n\tfor _, u := range uu {\n\t\tassert.Equal(t, u.e, toMi(u.v))\n\t}\n}\n\nfunc TestIntToStr(t *testing.T) {\n\tuu := []struct {\n\t\tv int\n\t\te string\n\t}{\n\t\t{0, \"0\"},\n\t\t{10, \"10\"},\n\t}\n\n\tfor _, u := range uu {\n\t\tassert.Equal(t, u.e, IntToStr(u.v))\n\t}\n}\n\nfunc BenchmarkIntToStr(b *testing.B) {\n\tv := 10\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\tfor range b.N {\n\t\tIntToStr(v)\n\t}\n}\n\n// Helpers...\n\nfunc load(t *testing.T, n string) *unstructured.Unstructured {\n\traw, err := os.ReadFile(fmt.Sprintf(\"testdata/%s.json\", n))\n\trequire.NoError(t, err)\n\tvar o unstructured.Unstructured\n\terr = json.Unmarshal(raw, &o)\n\trequire.NoError(t, err)\n\treturn &o\n}\n"
  },
  {
    "path": "internal/render/hpa.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\n// HorizontalPodAutoscaler renders a K8s HorizontalPodAutoscaler to screen.\ntype HorizontalPodAutoscaler struct {\n\tTable\n}\n\n// ColorerFunc colors a resource row.\nfunc (*HorizontalPodAutoscaler) ColorerFunc() model1.ColorerFunc {\n\treturn func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color {\n\t\tc := model1.DefaultColorer(ns, h, re)\n\n\t\tmaxPodsIndex, ok := h.IndexOf(\"MAXPODS\", true)\n\t\tif !ok || maxPodsIndex >= len(re.Row.Fields) {\n\t\t\treturn c\n\t\t}\n\n\t\treplicasIndex, ok := h.IndexOf(\"REPLICAS\", true)\n\t\tif !ok || replicasIndex >= len(re.Row.Fields) {\n\t\t\treturn c\n\t\t}\n\n\t\tmaxPodsS := strings.TrimSpace(re.Row.Fields[maxPodsIndex])\n\t\tcurrentReplicasS := strings.TrimSpace(re.Row.Fields[replicasIndex])\n\n\t\tmaxPods, err := strconv.Atoi(maxPodsS)\n\t\tif err != nil {\n\t\t\treturn c\n\t\t}\n\t\tcurrentReplicas, err := strconv.Atoi(currentReplicasS)\n\t\tif err != nil {\n\t\t\treturn c\n\t\t}\n\t\tif currentReplicas >= maxPods {\n\t\t\tc = model1.ErrColor\n\t\t}\n\t\treturn c\n\t}\n}\n"
  },
  {
    "path": "internal/render/hpa_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestHorizontalPodAutoscalerColorer(t *testing.T) {\n\thpaHeader := model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\t\tmodel1.HeaderColumn{Name: \"NAME\"},\n\t\tmodel1.HeaderColumn{Name: \"REFERENCE\"},\n\t\tmodel1.HeaderColumn{Name: \"TARGETS%\"},\n\t\tmodel1.HeaderColumn{Name: \"MINPODS\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\t\tmodel1.HeaderColumn{Name: \"MAXPODS\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\t\tmodel1.HeaderColumn{Name: \"REPLICAS\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\t\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n\t}\n\n\tuu := map[string]struct {\n\t\th  model1.Header\n\t\tre *model1.RowEvent\n\t\te  tcell.Color\n\t}{\n\t\t\"when replicas = maxpods\": {\n\t\t\th: hpaHeader,\n\t\t\tre: &model1.RowEvent{\n\t\t\t\tKind: model1.EventUnchanged,\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"blee\", \"fred\", \"fred\", \"100%\", \"1\", \"5\", \"5\", \"1d\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: model1.ErrColor,\n\t\t},\n\t\t\"when replicas > maxpods, for some reason\": {\n\t\t\th: hpaHeader,\n\t\t\tre: &model1.RowEvent{\n\t\t\t\tKind: model1.EventUnchanged,\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"blee\", \"fred\", \"fred\", \"100%\", \"1\", \"5\", \"6\", \"1d\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: model1.ErrColor,\n\t\t},\n\t\t\"when replicas < maxpods\": {\n\t\t\th: hpaHeader,\n\t\t\tre: &model1.RowEvent{\n\t\t\t\tKind: model1.EventUnchanged,\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"blee\", \"fred\", \"fred\", \"100%\", \"1\", \"5\", \"1\", \"1d\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: model1.StdColor,\n\t\t},\n\t}\n\n\tvar r HorizontalPodAutoscaler\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, r.ColorerFunc()(\"\", u.h, u.re))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/render/img_scan.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/vul\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n)\n\nconst (\n\tCVEParseIdx = 5\n\tsevColName  = \"SEVERITY\"\n)\n\n// ImageScan renders scans report table.\ntype ImageScan struct {\n\tBase\n}\n\n// ColorerFunc colors a resource row.\nfunc (ImageScan) ColorerFunc() model1.ColorerFunc {\n\treturn func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color {\n\t\tc := model1.DefaultColorer(ns, h, re)\n\n\t\tidx, ok := h.IndexOf(sevColName, true)\n\t\tif !ok {\n\t\t\treturn c\n\t\t}\n\t\tsev := strings.TrimSpace(re.Row.Fields[idx])\n\t\tswitch sev {\n\t\tcase vul.Sev1:\n\t\t\tc = tcell.ColorRed\n\t\tcase vul.Sev2:\n\t\t\tc = tcell.ColorDarkOrange\n\t\tcase vul.Sev3:\n\t\t\tc = tcell.ColorYellow\n\t\tcase vul.Sev4:\n\t\t\tc = tcell.ColorDeepSkyBlue\n\t\tcase vul.Sev5:\n\t\t\tc = tcell.ColorCadetBlue\n\t\tdefault:\n\t\t\tc = tcell.ColorDarkOliveGreen\n\t\t}\n\n\t\treturn c\n\t}\n}\n\n// Header returns a header row.\nfunc (ImageScan) Header(string) model1.Header {\n\treturn model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"SEVERITY\"},\n\t\tmodel1.HeaderColumn{Name: \"VULNERABILITY\"},\n\t\tmodel1.HeaderColumn{Name: \"IMAGE\"},\n\t\tmodel1.HeaderColumn{Name: \"LIBRARY\"},\n\t\tmodel1.HeaderColumn{Name: \"VERSION\"},\n\t\tmodel1.HeaderColumn{Name: \"FIXED-IN\"},\n\t\tmodel1.HeaderColumn{Name: \"TYPE\"},\n\t}\n}\n\n// Render renders a K8s resource to screen.\nfunc (ImageScan) Render(o any, _ string, r *model1.Row) error {\n\tres, ok := o.(ImageScanRes)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected ImageScanRes, but got %T\", o)\n\t}\n\n\tr.ID = fmt.Sprintf(\"%s|%s\", res.Image, strings.Join(res.Row, \"|\"))\n\tr.Fields = model1.Fields{\n\t\tres.Row.Severity(),\n\t\tres.Row.Vulnerability(),\n\t\tres.Image,\n\t\tres.Row.Name(),\n\t\tres.Row.Version(),\n\t\tres.Row.Fix(),\n\t\tres.Row.Type(),\n\t}\n\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\n// ImageScanRes represents a container and its metrics.\ntype ImageScanRes struct {\n\tImage string\n\tRow   vul.Row\n}\n\n// GetObjectKind returns a schema object.\nfunc (ImageScanRes) GetObjectKind() schema.ObjectKind {\n\treturn nil\n}\n\n// DeepCopyObject returns a container copy.\nfunc (is ImageScanRes) DeepCopyObject() runtime.Object {\n\treturn is\n}\n"
  },
  {
    "path": "internal/render/job.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\tbatchv1 \"k8s.io/api/batch/v1\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/util/duration\"\n)\n\nvar defaultJOBHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"VS\", Attrs: model1.Attrs{VS: true}},\n\tmodel1.HeaderColumn{Name: \"COMPLETIONS\"},\n\tmodel1.HeaderColumn{Name: \"DURATION\"},\n\tmodel1.HeaderColumn{Name: \"SELECTOR\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"CONTAINERS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"IMAGES\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// Job renders a K8s Job to screen.\ntype Job struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (j Job) Header(_ string) model1.Header {\n\treturn j.doHeader(defaultJOBHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (j Job) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tif err := j.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif j.specs.isEmpty() {\n\t\treturn nil\n\t}\n\tcols, err := j.specs.realize(raw, defaultJOBHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\nfunc (j Job) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {\n\tvar job batchv1.Job\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &job)\n\tif err != nil {\n\t\treturn err\n\t}\n\tready := toCompletion(&job.Spec, &job.Status)\n\n\tcc, ii := toContainers(&job.Spec.Template.Spec)\n\n\tr.ID = client.MetaFQN(&job.ObjectMeta)\n\tr.Fields = model1.Fields{\n\t\tjob.Namespace,\n\t\tjob.Name,\n\t\tcomputeVulScore(job.Namespace, job.Labels, &job.Spec.Template.Spec),\n\t\tready,\n\t\ttoDuration(&job.Status),\n\t\tjobSelector(&job.Spec),\n\t\tcc,\n\t\tii,\n\t\tAsStatus(j.diagnose(ready, &job.Status)),\n\t\tToAge(job.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n\nfunc (Job) diagnose(ready string, status *batchv1.JobStatus) error {\n\ttokens := strings.Split(ready, \"/\")\n\tif tokens[0] != tokens[1] && status.Failed > 0 {\n\t\treturn fmt.Errorf(\"%d pods failed\", status.Failed)\n\t}\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nconst maxShow = 2\n\nfunc toContainers(p *v1.PodSpec) (containers, images string) {\n\tcc, ii := parseContainers(p.InitContainers)\n\tcn, ci := parseContainers(p.Containers)\n\n\tcc, ii = append(cc, cn...), append(ii, ci...)\n\n\t// Limit to 2 of each...\n\tif len(cc) > maxShow {\n\t\tcc = append(cc[:2], \"(+\"+strconv.Itoa(len(cc)-maxShow)+\")...\")\n\t}\n\tif len(ii) > maxShow {\n\t\tii = append(ii[:2], \"(+\"+strconv.Itoa(len(ii)-maxShow)+\")...\")\n\t}\n\n\treturn strings.Join(cc, \",\"), strings.Join(ii, \",\")\n}\n\nfunc parseContainers(cos []v1.Container) (nn, ii []string) {\n\tnn, ii = make([]string, 0, len(cos)), make([]string, 0, len(cos))\n\tfor i := range cos {\n\t\tnn, ii = append(nn, cos[i].Name), append(ii, cos[i].Image)\n\t}\n\n\treturn nn, ii\n}\n\nfunc toCompletion(spec *batchv1.JobSpec, status *batchv1.JobStatus) (s string) {\n\tif spec.Completions != nil {\n\t\treturn strconv.Itoa(int(status.Succeeded)) + \"/\" + strconv.Itoa(int(*spec.Completions))\n\t}\n\n\tif spec.Parallelism == nil {\n\t\treturn strconv.Itoa(int(status.Succeeded)) + \"/1\"\n\t}\n\n\tp := *spec.Parallelism\n\tif p > 1 {\n\t\treturn strconv.Itoa(int(status.Succeeded)) + \"/1 of \" + strconv.Itoa(int(p))\n\t}\n\n\treturn strconv.Itoa(int(status.Succeeded)) + \"/1\"\n}\n\nfunc toDuration(status *batchv1.JobStatus) string {\n\tif status.StartTime == nil || status.CompletionTime == nil {\n\t\treturn MissingValue\n\t}\n\n\treturn duration.HumanDuration(status.CompletionTime.Sub(status.StartTime.Time))\n}\n"
  },
  {
    "path": "internal/render/job_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestJobRender(t *testing.T) {\n\tc := render.Job{}\n\tr := model1.NewRow(4)\n\n\trequire.NoError(t, c.Render(load(t, \"job\"), \"\", &r))\n\tassert.Equal(t, \"default/hello-1567179180\", r.ID)\n\tassert.Equal(t, model1.Fields{\"default\", \"hello-1567179180\", \"n/a\", \"1/1\", \"8s\", \"controller-uid=7473e6d0-cb3b-11e9-990f-42010a800218\", \"c1\", \"blang/busybox-bash\"}, r.Fields[:8])\n}\n"
  },
  {
    "path": "internal/render/node.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/api/resource\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n\tmv1beta1 \"k8s.io/metrics/pkg/apis/metrics/v1beta1\"\n)\n\nconst (\n\tlabelNodeRolePrefix = \"node-role.kubernetes.io/\"\n\tlabelNodeRoleSuffix = \"kubernetes.io/role\"\n)\n\nvar (\n\tcordonErr   = errors.New(\"node is cordoned\")\n\tnotReadyErr = errors.New(\"node is not ready\")\n)\n\nvar defaultNOHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"STATUS\"},\n\tmodel1.HeaderColumn{Name: \"ROLE\"},\n\tmodel1.HeaderColumn{Name: \"ARCH\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"TAINTS\"},\n\tmodel1.HeaderColumn{Name: \"VERSION\"},\n\tmodel1.HeaderColumn{Name: \"OS-IMAGE\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"KERNEL\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"INTERNAL-IP\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"EXTERNAL-IP\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"PODS\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"CPU\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"CPU/A\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"%CPU\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"MEM\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"MEM/A\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"%MEM\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"GPU/A\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"GPU/C\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"SH-GPU/A\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"SH-GPU/C\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"LABELS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// Node renders a K8s Node to screen.\ntype Node struct {\n\tBase\n}\n\n// ColorerFunc colors a resource row.\nfunc (*Node) ColorerFunc() model1.ColorerFunc {\n\treturn func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color {\n\t\tc := model1.DefaultColorer(ns, h, re)\n\n\t\tidx, ok := h.IndexOf(\"VALID\", true)\n\t\tif !ok {\n\t\t\treturn c\n\t\t}\n\t\tif strings.TrimSpace(re.Row.Fields[idx]) == cordonErr.Error() {\n\t\t\tc = model1.PendingColor\n\t\t}\n\n\t\treturn c\n\t}\n}\n\n// Header returns a header row.\nfunc (n Node) Header(_ string) model1.Header {\n\treturn n.doHeader(defaultNOHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (n Node) Render(o any, _ string, row *model1.Row) error {\n\tnwm, ok := o.(*NodeWithMetrics)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected NodeWithMetrics, but got %T\", o)\n\t}\n\tif err := n.defaultRow(nwm, row); err != nil {\n\t\treturn err\n\t}\n\tif n.specs.isEmpty() {\n\t\treturn nil\n\t}\n\n\tcols, err := n.specs.realize(nwm.Raw, defaultNOHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\n// Render renders a K8s resource to screen.\nfunc (n Node) defaultRow(nwm *NodeWithMetrics, r *model1.Row) error {\n\tvar no v1.Node\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(nwm.Raw.Object, &no)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tiIP, eIP := getIPs(no.Status.Addresses)\n\tiIP, eIP = missing(iIP), missing(eIP)\n\n\tc, a := gatherNodeMX(&no, nwm.MX)\n\n\tstatuses := make(sort.StringSlice, 10)\n\tstatus(no.Status.Conditions, no.Spec.Unschedulable, statuses)\n\tsort.Sort(statuses)\n\troles := make(sort.StringSlice, 10)\n\tnodeRoles(&no, roles)\n\tsort.Sort(roles)\n\n\tpodCount := strconv.Itoa(nwm.PodCount)\n\tif pc := nwm.PodCount; pc == -1 {\n\t\tpodCount = NAValue\n\t}\n\tr.ID = client.FQN(\"\", no.Name)\n\tr.Fields = model1.Fields{\n\t\tno.Name,\n\t\tjoin(statuses, \",\"),\n\t\tjoin(roles, \",\"),\n\t\tno.Status.NodeInfo.Architecture,\n\t\tstrconv.Itoa(len(no.Spec.Taints)),\n\t\tno.Status.NodeInfo.KubeletVersion,\n\t\tno.Status.NodeInfo.OSImage,\n\t\tno.Status.NodeInfo.KernelVersion,\n\t\tiIP,\n\t\teIP,\n\t\tpodCount,\n\t\ttoMc(c.cpu),\n\t\ttoMc(a.cpu),\n\t\tclient.ToPercentageStr(c.cpu, a.cpu),\n\t\ttoMi(c.mem),\n\t\ttoMi(a.mem),\n\t\tclient.ToPercentageStr(c.mem, a.mem),\n\t\ttoMu(a.gpu),\n\t\ttoMu(c.gpu),\n\t\ttoMu(a.gpuShared),\n\t\ttoMu(c.gpuShared),\n\t\tmapToStr(no.Labels),\n\t\tAsStatus(n.diagnose(statuses)),\n\t\tToAge(no.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n\n// Healthy checks component health.\nfunc (n Node) Healthy(_ context.Context, o any) error {\n\tnwm, ok := o.(*NodeWithMetrics)\n\tif !ok {\n\t\tslog.Error(\"Expected *NodeWithMetrics\", slogs.Type, fmt.Sprintf(\"%T\", o))\n\t\treturn nil\n\t}\n\tvar no v1.Node\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(nwm.Raw.Object, &no)\n\tif err != nil {\n\t\tslog.Error(\"Failed to convert unstructured to Node\", slogs.Error, err)\n\t\treturn nil\n\t}\n\tss := make([]string, 10)\n\tstatus(no.Status.Conditions, no.Spec.Unschedulable, ss)\n\n\treturn n.diagnose(ss)\n}\n\nfunc (Node) diagnose(ss []string) error {\n\tif len(ss) == 0 {\n\t\treturn nil\n\t}\n\n\tvar ready bool\n\tfor _, s := range ss {\n\t\tif s == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tif s == \"SchedulingDisabled\" {\n\t\t\treturn cordonErr\n\t\t}\n\t\tif s == \"Ready\" {\n\t\t\tready = true\n\t\t}\n\t}\n\n\tif !ready {\n\t\treturn notReadyErr\n\t}\n\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\n// NodeWithMetrics represents a node with its associated metrics.\ntype NodeWithMetrics struct {\n\tRaw      *unstructured.Unstructured\n\tMX       *mv1beta1.NodeMetrics\n\tPodCount int\n}\n\n// GetObjectKind returns a schema object.\nfunc (*NodeWithMetrics) GetObjectKind() schema.ObjectKind {\n\treturn nil\n}\n\n// DeepCopyObject returns a container copy.\nfunc (n *NodeWithMetrics) DeepCopyObject() runtime.Object {\n\treturn n\n}\n\ntype metric struct {\n\tcpu, mem       int64\n\tlcpu, lmem     int64\n\tgpu, gpuShared int64\n\tlgpu           int64\n}\n\nfunc gatherNodeMX(no *v1.Node, mx *mv1beta1.NodeMetrics) (c, a metric) {\n\ta.cpu = no.Status.Allocatable.Cpu().MilliValue()\n\ta.mem = no.Status.Allocatable.Memory().Value()\n\tif mx != nil {\n\t\tc.cpu = mx.Usage.Cpu().MilliValue()\n\t\tc.mem = mx.Usage.Memory().Value()\n\t}\n\n\tgpu, gpuShared := extractNodeGPU(no.Status.Allocatable)\n\tif gpu != nil {\n\t\ta.gpu = gpu.Value()\n\t}\n\tif gpuShared != nil {\n\t\ta.gpuShared = gpuShared.Value()\n\t}\n\tgpu, gpuShared = extractNodeGPU(no.Status.Capacity)\n\tif gpu != nil {\n\t\tc.gpu = gpu.Value()\n\t}\n\tif gpuShared != nil {\n\t\tc.gpuShared = gpuShared.Value()\n\t}\n\n\treturn\n}\n\nfunc extractNodeGPU(rl v1.ResourceList) (main, shared *resource.Quantity) {\n\tmm := make(map[string]*resource.Quantity, len(config.KnownGPUVendors))\n\tfor _, v := range config.KnownGPUVendors {\n\t\tif q, ok := rl[v1.ResourceName(v)]; ok {\n\t\t\tmm[v] = &q\n\t\t}\n\t}\n\tfor k, v := range mm {\n\t\tif strings.HasSuffix(k, \"shared\") {\n\t\t\tshared = v\n\t\t} else {\n\t\t\tmain = v\n\t\t}\n\t}\n\n\treturn\n}\n\nfunc nodeRoles(node *v1.Node, res []string) {\n\tindex := 0\n\tfor k, v := range node.Labels {\n\t\tswitch {\n\t\tcase strings.HasPrefix(k, labelNodeRolePrefix):\n\t\t\tif role := strings.TrimPrefix(k, labelNodeRolePrefix); role != \"\" {\n\t\t\t\tres[index] = role\n\t\t\t\tindex++\n\t\t\t}\n\t\tcase strings.HasSuffix(k, labelNodeRoleSuffix) && v != \"\":\n\t\t\tres[index] = v\n\t\t\tindex++\n\t\t}\n\t\tif index >= len(res) {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif blank(res) {\n\t\tres[index] = MissingValue\n\t}\n}\n\nfunc getIPs(addrs []v1.NodeAddress) (iIP, eIP string) {\n\tfor _, a := range addrs {\n\t\t//nolint:exhaustive\n\t\tswitch a.Type {\n\t\tcase v1.NodeExternalIP:\n\t\t\teIP = a.Address\n\t\tcase v1.NodeInternalIP:\n\t\t\tiIP = a.Address\n\t\t}\n\t}\n\n\treturn\n}\n\nfunc status(conds []v1.NodeCondition, exempt bool, res []string) {\n\tvar index int\n\tconditions := make(map[v1.NodeConditionType]*v1.NodeCondition, len(conds))\n\tfor n := range conds {\n\t\tcond := conds[n]\n\t\tconditions[cond.Type] = &cond\n\t}\n\n\tvalidConditions := []v1.NodeConditionType{v1.NodeReady}\n\tfor _, validCondition := range validConditions {\n\t\tcondition, ok := conditions[validCondition]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\t\tneg := \"\"\n\t\tif condition.Status != v1.ConditionTrue {\n\t\t\tneg = \"Not\"\n\t\t}\n\t\tres[index] = neg + string(condition.Type)\n\t\tindex++\n\t}\n\tif len(res) == 0 {\n\t\tres[index] = \"Unknown\"\n\t\tindex++\n\t}\n\tif exempt {\n\t\tres[index] = \"SchedulingDisabled\"\n\t}\n}\n"
  },
  {
    "path": "internal/render/node_int_test.go",
    "content": "package render\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/api/resource\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\tmv1beta1 \"k8s.io/metrics/pkg/apis/metrics/v1beta1\"\n)\n\nfunc Test_extractNodeGPU(t *testing.T) {\n\tuu := map[string]struct {\n\t\trl     v1.ResourceList\n\t\tmain   *resource.Quantity\n\t\tshared *resource.Quantity\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"nvidia\": {\n\t\t\trl: v1.ResourceList{\n\t\t\t\tv1.ResourceCPU:                    resource.MustParse(\"3\"),\n\t\t\t\tv1.ResourceMemory:                 resource.MustParse(\"4Gi\"),\n\t\t\t\tv1.ResourceName(\"nvidia.com/gpu\"): resource.MustParse(\"2\"),\n\t\t\t},\n\t\t\tmain: makeQ(t, \"2\"),\n\t\t},\n\n\t\t\"nvidia-shared\": {\n\t\t\trl: v1.ResourceList{\n\t\t\t\tv1.ResourceCPU:                           resource.MustParse(\"3\"),\n\t\t\t\tv1.ResourceMemory:                        resource.MustParse(\"4Gi\"),\n\t\t\t\tv1.ResourceName(\"nvidia.com/gpu.shared\"): resource.MustParse(\"2\"),\n\t\t\t},\n\t\t\tshared: makeQ(t, \"2\"),\n\t\t},\n\n\t\t\"nvidia-both\": {\n\t\t\trl: v1.ResourceList{\n\t\t\t\tv1.ResourceCPU:                           resource.MustParse(\"3\"),\n\t\t\t\tv1.ResourceMemory:                        resource.MustParse(\"4Gi\"),\n\t\t\t\tv1.ResourceName(\"nvidia.com/gpu.shared\"): resource.MustParse(\"2\"),\n\t\t\t\tv1.ResourceName(\"nvidia.com/gpu\"):        resource.MustParse(\"5\"),\n\t\t\t},\n\t\t\tmain:   makeQ(t, \"5\"),\n\t\t\tshared: makeQ(t, \"2\"),\n\t\t},\n\n\t\t\"intel\": {\n\t\t\trl: v1.ResourceList{\n\t\t\t\tv1.ResourceCPU:                        resource.MustParse(\"3\"),\n\t\t\t\tv1.ResourceMemory:                     resource.MustParse(\"4Gi\"),\n\t\t\t\tv1.ResourceName(\"gpu.intel.com/i915\"): resource.MustParse(\"5\"),\n\t\t\t},\n\t\t\tmain: makeQ(t, \"5\"),\n\t\t},\n\n\t\t\"unknown-vendor\": {\n\t\t\trl: v1.ResourceList{\n\t\t\t\tv1.ResourceCPU:              resource.MustParse(\"3\"),\n\t\t\t\tv1.ResourceMemory:           resource.MustParse(\"4Gi\"),\n\t\t\t\tv1.ResourceName(\"bozo/gpu\"): resource.MustParse(\"2\"),\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tm, s := extractNodeGPU(u.rl)\n\t\t\tassert.Equal(t, u.main, m)\n\t\t\tassert.Equal(t, u.shared, s)\n\t\t})\n\t}\n}\n\nfunc Test_gatherNodeMX(t *testing.T) {\n\tuu := map[string]struct {\n\t\tnode   v1.Node\n\t\tnMX    *mv1beta1.NodeMetrics\n\t\tec, ea metric\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"nvidia\": {\n\t\t\tnode: v1.Node{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tName: \"nvidia\",\n\t\t\t\t},\n\t\t\t\tStatus: v1.NodeStatus{\n\t\t\t\t\tCapacity: v1.ResourceList{\n\t\t\t\t\t\tv1.ResourceCPU:                    resource.MustParse(\"3\"),\n\t\t\t\t\t\tv1.ResourceMemory:                 resource.MustParse(\"4Gi\"),\n\t\t\t\t\t\tv1.ResourceName(\"nvidia.com/gpu\"): resource.MustParse(\"2\"),\n\t\t\t\t\t},\n\t\t\t\t\tAllocatable: v1.ResourceList{\n\t\t\t\t\t\tv1.ResourceCPU:                    resource.MustParse(\"8\"),\n\t\t\t\t\t\tv1.ResourceMemory:                 resource.MustParse(\"8Gi\"),\n\t\t\t\t\t\tv1.ResourceName(\"nvidia.com/gpu\"): resource.MustParse(\"4\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tnMX: &mv1beta1.NodeMetrics{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tName: \"nvidia\",\n\t\t\t\t},\n\t\t\t\tUsage: v1.ResourceList{\n\t\t\t\t\tv1.ResourceCPU:                    resource.MustParse(\"3\"),\n\t\t\t\t\tv1.ResourceMemory:                 resource.MustParse(\"4Gi\"),\n\t\t\t\t\tv1.ResourceName(\"nvidia.com/gpu\"): resource.MustParse(\"2\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tea: metric{\n\t\t\t\tcpu: 8000,\n\t\t\t\tmem: 8589934592,\n\t\t\t\tgpu: 4,\n\t\t\t},\n\t\t\tec: metric{\n\t\t\t\tcpu: 3000,\n\t\t\t\tmem: 4294967296,\n\t\t\t\tgpu: 2,\n\t\t\t},\n\t\t},\n\n\t\t\"intel\": {\n\t\t\tnode: v1.Node{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tName: \"intel\",\n\t\t\t\t},\n\t\t\t\tStatus: v1.NodeStatus{\n\t\t\t\t\tCapacity: v1.ResourceList{\n\t\t\t\t\t\tv1.ResourceCPU:                        resource.MustParse(\"3\"),\n\t\t\t\t\t\tv1.ResourceMemory:                     resource.MustParse(\"4Gi\"),\n\t\t\t\t\t\tv1.ResourceName(\"gpu.intel.com/i915\"): resource.MustParse(\"2\"),\n\t\t\t\t\t},\n\t\t\t\t\tAllocatable: v1.ResourceList{\n\t\t\t\t\t\tv1.ResourceCPU:                        resource.MustParse(\"8\"),\n\t\t\t\t\t\tv1.ResourceMemory:                     resource.MustParse(\"8Gi\"),\n\t\t\t\t\t\tv1.ResourceName(\"gpu.intel.com/i915\"): resource.MustParse(\"4\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tea: metric{\n\t\t\t\tcpu: 8000,\n\t\t\t\tmem: 8589934592,\n\t\t\t\tgpu: 4,\n\t\t\t},\n\t\t\tec: metric{\n\t\t\t\tcpu: 0,\n\t\t\t\tmem: 0,\n\t\t\t\tgpu: 2,\n\t\t\t},\n\t\t},\n\n\t\t\"unknown-vendor\": {\n\t\t\tnode: v1.Node{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tName: \"amd\",\n\t\t\t\t},\n\t\t\t\tStatus: v1.NodeStatus{\n\t\t\t\t\tCapacity: v1.ResourceList{\n\t\t\t\t\t\tv1.ResourceCPU:              resource.MustParse(\"3\"),\n\t\t\t\t\t\tv1.ResourceMemory:           resource.MustParse(\"4Gi\"),\n\t\t\t\t\t\tv1.ResourceName(\"bozo/gpu\"): resource.MustParse(\"2\"),\n\t\t\t\t\t},\n\t\t\t\t\tAllocatable: v1.ResourceList{\n\t\t\t\t\t\tv1.ResourceCPU:              resource.MustParse(\"8\"),\n\t\t\t\t\t\tv1.ResourceMemory:           resource.MustParse(\"8Gi\"),\n\t\t\t\t\t\tv1.ResourceName(\"bozo/gpu\"): resource.MustParse(\"4\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tea: metric{\n\t\t\t\tcpu: 8000,\n\t\t\t\tmem: 8589934592,\n\t\t\t\tgpu: 0,\n\t\t\t},\n\t\t\tec: metric{\n\t\t\t\tgpu: 0,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tc, a := gatherNodeMX(&u.node, u.nMX)\n\t\t\tassert.Equal(t, u.ec, c)\n\t\t\tassert.Equal(t, u.ea, a)\n\t\t})\n\t}\n}\n\nfunc makeQ(t *testing.T, v string) *resource.Quantity {\n\tq, err := resource.ParseQuantity(v)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\treturn &q\n}\n"
  },
  {
    "path": "internal/render/node_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\tmv1beta1 \"k8s.io/metrics/pkg/apis/metrics/v1beta1\"\n)\n\nfunc TestNodeRender(t *testing.T) {\n\tpom := render.NodeWithMetrics{\n\t\tRaw: load(t, \"no\"),\n\t\tMX:  makeNodeMX(\"n1\", \"10m\", \"20Mi\"),\n\t}\n\n\tvar no render.Node\n\tr := model1.NewRow(14)\n\terr := no.Render(&pom, \"\", &r)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"minikube\", r.ID)\n\te := model1.Fields{\"minikube\", \"Ready\", \"master\", \"amd64\", \"0\", \"v1.15.2\", \"Buildroot 2018.05.3\", \"4.15.0\", \"192.168.64.107\", \"<none>\", \"0\", \"10\", \"4000\", \"0\", \"20\", \"7874\", \"0\", \"n/a\", \"n/a\"}\n\tassert.Equal(t, e, r.Fields[:19])\n}\n\nfunc BenchmarkNodeRender(b *testing.B) {\n\tvar (\n\t\tno  render.Node\n\t\tr   = model1.NewRow(14)\n\t\tpom = render.NodeWithMetrics{\n\t\t\tRaw: load(b, \"no\"),\n\t\t\tMX:  makeNodeMX(\"n1\", \"10m\", \"10Mi\"),\n\t\t}\n\t)\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\tfor range b.N {\n\t\t_ = no.Render(&pom, \"\", &r)\n\t}\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc makeNodeMX(name, cpu, mem string) *mv1beta1.NodeMetrics {\n\treturn &mv1beta1.NodeMetrics{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:      name,\n\t\t\tNamespace: \"default\",\n\t\t},\n\t\tUsage: makeRes(cpu, mem),\n\t}\n}\n"
  },
  {
    "path": "internal/render/np.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\tnetv1 \"k8s.io/api/networking/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar defaultNPHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"POD-SELECTOR\"},\n\tmodel1.HeaderColumn{Name: \"ING-SELECTOR\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"ING-PORTS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"ING-BLOCK\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"EGR-SELECTOR\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"EGR-PORTS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"EGR-BLOCK\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"LABELS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// NetworkPolicy renders a K8s NetworkPolicy to screen.\ntype NetworkPolicy struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (p NetworkPolicy) Header(_ string) model1.Header {\n\treturn p.doHeader(defaultNPHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (p NetworkPolicy) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tif err := p.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif p.specs.isEmpty() {\n\t\treturn nil\n\t}\n\n\tcols, err := p.specs.realize(raw, defaultNPHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\nfunc (NetworkPolicy) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {\n\tvar np netv1.NetworkPolicy\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &np)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tip, is, ib := ingress(np.Spec.Ingress)\n\tep, es, eb := egress(np.Spec.Egress)\n\n\tvar podSel string\n\tif len(np.Spec.PodSelector.MatchLabels) > 0 {\n\t\tpodSel = mapToStr(np.Spec.PodSelector.MatchLabels)\n\t}\n\tif len(np.Spec.PodSelector.MatchExpressions) > 0 {\n\t\tpodSel += \"::\" + expToStr(np.Spec.PodSelector.MatchExpressions)\n\t}\n\tr.ID = client.MetaFQN(&np.ObjectMeta)\n\tr.Fields = model1.Fields{\n\t\tnp.Namespace,\n\t\tnp.Name,\n\t\tpodSel,\n\t\tis,\n\t\tip,\n\t\tib,\n\t\tes,\n\t\tep,\n\t\teb,\n\t\tmapToStr(np.Labels),\n\t\t\"\",\n\t\tToAge(np.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n\n// Helpers...\n\nfunc ingress(ii []netv1.NetworkPolicyIngressRule) (port, selector, block string) {\n\tvar ports, sels, blocks []string\n\tfor _, i := range ii {\n\t\tif p := portsToStr(i.Ports); p != \"\" {\n\t\t\tports = append(ports, p)\n\t\t}\n\t\tll, pp := peersToStr(i.From)\n\t\tif ll != \"\" {\n\t\t\tsels = append(sels, ll)\n\t\t}\n\t\tif pp != \"\" {\n\t\t\tblocks = append(blocks, pp)\n\t\t}\n\t}\n\treturn strings.Join(ports, \",\"), strings.Join(sels, \",\"), strings.Join(blocks, \",\")\n}\n\nfunc egress(ee []netv1.NetworkPolicyEgressRule) (port, selector, block string) {\n\tvar ports, sels, blocks []string\n\tfor _, e := range ee {\n\t\tif p := portsToStr(e.Ports); p != \"\" {\n\t\t\tports = append(ports, p)\n\t\t}\n\t\tll, pp := peersToStr(e.To)\n\t\tif ll != \"\" {\n\t\t\tsels = append(sels, ll)\n\t\t}\n\t\tif pp != \"\" {\n\t\t\tblocks = append(blocks, pp)\n\t\t}\n\t}\n\treturn strings.Join(ports, \",\"), strings.Join(sels, \",\"), strings.Join(blocks, \",\")\n}\n\nfunc portsToStr(pp []netv1.NetworkPolicyPort) string {\n\tports := make([]string, 0, len(pp))\n\tfor _, p := range pp {\n\t\tproto, port := NAValue, NAValue\n\t\tif p.Protocol != nil {\n\t\t\tproto = string(*p.Protocol)\n\t\t}\n\t\tif p.Port != nil {\n\t\t\tport = p.Port.String()\n\t\t}\n\t\tports = append(ports, proto+\":\"+port)\n\t}\n\treturn strings.Join(ports, \",\")\n}\n\nfunc peersToStr(pp []netv1.NetworkPolicyPeer) (selector, ip string) {\n\tsels := make([]string, 0, len(pp))\n\tips := make([]string, 0, len(pp))\n\tfor _, p := range pp {\n\t\tif peer := renderPeer(p); peer != \"\" {\n\t\t\tsels = append(sels, peer)\n\t\t}\n\n\t\tif p.IPBlock == nil {\n\t\t\tcontinue\n\t\t}\n\t\tif b := renderBlock(p.IPBlock); b != \"\" {\n\t\t\tips = append(ips, b)\n\t\t}\n\t}\n\treturn strings.Join(sels, \",\"), strings.Join(ips, \",\")\n}\n\nfunc renderBlock(b *netv1.IPBlock) string {\n\ts := b.CIDR\n\n\tif len(b.Except) == 0 {\n\t\treturn s\n\t}\n\n\te, more := b.Except, false\n\tif len(b.Except) > 2 {\n\t\te, more = e[:2], true\n\t}\n\tif more {\n\t\treturn s + \"[\" + strings.Join(e, \",\") + \"...]\"\n\t}\n\treturn s + \"[\" + strings.Join(b.Except, \",\") + \"]\"\n}\n\nfunc renderPeer(i netv1.NetworkPolicyPeer) string {\n\tvar s string\n\n\tif i.PodSelector != nil {\n\t\tif m := mapToStr(i.PodSelector.MatchLabels); m != \"\" {\n\t\t\ts += \"po:\" + m\n\t\t}\n\t\tif e := expToStr(i.PodSelector.MatchExpressions); e != \"\" {\n\t\t\ts += \"--\" + e\n\t\t}\n\t}\n\n\tif i.NamespaceSelector != nil {\n\t\tif m := mapToStr(i.NamespaceSelector.MatchLabels); m != \"\" {\n\t\t\ts += \"ns:\" + m\n\t\t}\n\t\tif e := expToStr(i.NamespaceSelector.MatchExpressions); e != \"\" {\n\t\t\ts += \"--\" + e\n\t\t}\n\t}\n\n\treturn s\n}\n\nfunc expToStr(ee []metav1.LabelSelectorRequirement) string {\n\tss := make([]string, len(ee))\n\tfor i, e := range ee {\n\t\tss[i] = labToStr(e)\n\t}\n\treturn strings.Join(ss, \",\")\n}\n\nfunc labToStr(e metav1.LabelSelectorRequirement) string {\n\treturn fmt.Sprintf(\"%s-%s%s\", e.Key, e.Operator, strings.Join(e.Values, \",\"))\n}\n"
  },
  {
    "path": "internal/render/np_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNetworkPolicyRender(t *testing.T) {\n\tc := render.NetworkPolicy{}\n\tr := model1.NewRow(9)\n\n\trequire.NoError(t, c.Render(load(t, \"np\"), \"\", &r))\n\tassert.Equal(t, \"default/fred\", r.ID)\n\tassert.Equal(t, model1.Fields{\"default\", \"fred\", \"app=nginx\", \"ns:app=blee,po:app=fred\", \"TCP:6379\", \"172.17.0.0/16[172.17.1.0/24,172.17.3.0/24...]\", \"\", \"TCP:5978\", \"10.0.0.0/24\"}, r.Fields[:9])\n}\n"
  },
  {
    "path": "internal/render/ns.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"golang.org/x/exp/slog\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar defaultNSHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"STATUS\"},\n\tmodel1.HeaderColumn{Name: \"LABELS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// Namespace renders a K8s Namespace to screen.\ntype Namespace struct {\n\tBase\n}\n\n// ColorerFunc colors a resource row.\nfunc (Namespace) ColorerFunc() model1.ColorerFunc {\n\treturn func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color {\n\t\tc := model1.DefaultColorer(ns, h, re)\n\t\tif c == model1.ErrColor {\n\t\t\treturn c\n\t\t}\n\t\tif re.Kind == model1.EventUpdate {\n\t\t\tc = model1.StdColor\n\t\t}\n\t\tif strings.Contains(strings.TrimSpace(re.Row.Fields[0]), \"*\") {\n\t\t\tc = model1.HighlightColor\n\t\t}\n\n\t\treturn c\n\t}\n}\n\n// Header returns a header row.\nfunc (n Namespace) Header(_ string) model1.Header {\n\treturn n.doHeader(defaultNSHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (n Namespace) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tif err := n.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif n.specs.isEmpty() {\n\t\treturn nil\n\t}\n\n\tcols, err := n.specs.realize(raw, defaultNSHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\nfunc (n Namespace) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {\n\tvar ns v1.Namespace\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ns)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tr.ID = client.MetaFQN(&ns.ObjectMeta)\n\tr.Fields = model1.Fields{\n\t\tns.Name,\n\t\tstring(ns.Status.Phase),\n\t\tmapToStr(ns.Labels),\n\t\tAsStatus(n.diagnose(ns.Status.Phase)),\n\t\tToAge(ns.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n\n// Healthy checks component health.\nfunc (n Namespace) Healthy(_ context.Context, o any) error {\n\tres, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\tslog.Error(\"Expected *Unstructured, but got\", slogs.Type, fmt.Sprintf(\"%T\", o))\n\t\treturn nil\n\t}\n\tvar ns v1.Namespace\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(res.Object, &ns)\n\tif err != nil {\n\t\tslog.Error(\"Failed to convert Unstructured to Namespace\", slogs.Type, fmt.Sprintf(\"%T\", o), slog.String(\"error\", err.Error()))\n\t\treturn nil\n\t}\n\n\treturn n.diagnose(ns.Status.Phase)\n}\n\nfunc (Namespace) diagnose(phase v1.NamespacePhase) error {\n\tif phase != v1.NamespaceActive && phase != v1.NamespaceTerminating {\n\t\treturn errors.New(\"namespace not ready\")\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/render/ns_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNSColorer(t *testing.T) {\n\tuu := map[string]struct {\n\t\tre model1.RowEvent\n\t\te  tcell.Color\n\t}{\n\t\t\"add\": {\n\t\t\tre: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\n\t\t\t\t\t\t\"blee\",\n\t\t\t\t\t\t\"Active\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: model1.AddColor,\n\t\t},\n\t\t\"update\": {\n\t\t\tre: model1.RowEvent{\n\t\t\t\tKind: model1.EventUpdate,\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\n\t\t\t\t\t\t\"blee\",\n\t\t\t\t\t\t\"Active\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: model1.StdColor,\n\t\t},\n\t\t\"decorator\": {\n\t\t\tre: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\n\t\t\t\t\t\t\"blee*\",\n\t\t\t\t\t\t\"Active\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: model1.HighlightColor,\n\t\t},\n\t}\n\n\th := model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"NAME\"},\n\t\tmodel1.HeaderColumn{Name: \"STATUS\"},\n\t}\n\n\tvar r render.Namespace\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, r.ColorerFunc()(\"\", h, &u.re))\n\t\t})\n\t}\n}\n\nfunc TestNamespaceRender(t *testing.T) {\n\tc := render.Namespace{}\n\tr := model1.NewRow(3)\n\n\trequire.NoError(t, c.Render(load(t, \"ns\"), \"-\", &r))\n\tassert.Equal(t, \"-/kube-system\", r.ID)\n\tassert.Equal(t, model1.Fields{\"kube-system\", \"Active\"}, r.Fields[:2])\n}\n"
  },
  {
    "path": "internal/render/pdb.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/tview\"\n\tv1 \"k8s.io/api/policy/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/util/intstr\"\n)\n\nvar defaultPDBHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"MIN-AVAILABLE\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"MAX-UNAVAILABLE\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"ALLOWED-DISRUPTIONS\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"CURRENT\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"DESIRED\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"EXPECTED\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"LABELS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// PodDisruptionBudget renders a K8s PodDisruptionBudget to screen.\ntype PodDisruptionBudget struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (p PodDisruptionBudget) Header(_ string) model1.Header {\n\treturn p.doHeader(defaultPDBHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (p PodDisruptionBudget) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tif err := p.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif p.specs.isEmpty() {\n\t\treturn nil\n\t}\n\n\tcols, err := p.specs.realize(raw, defaultPDBHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\nfunc (p PodDisruptionBudget) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {\n\tvar pdb v1.PodDisruptionBudget\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &pdb)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tr.ID = client.MetaFQN(&pdb.ObjectMeta)\n\tr.Fields = model1.Fields{\n\t\tpdb.Namespace,\n\t\tpdb.Name,\n\t\tnumbToStr(pdb.Spec.MinAvailable),\n\t\tnumbToStr(pdb.Spec.MaxUnavailable),\n\t\tstrconv.Itoa(int(pdb.Status.DisruptionsAllowed)),\n\t\tstrconv.Itoa(int(pdb.Status.CurrentHealthy)),\n\t\tstrconv.Itoa(int(pdb.Status.DesiredHealthy)),\n\t\tstrconv.Itoa(int(pdb.Status.ExpectedPods)),\n\t\tmapToStr(pdb.Labels),\n\t\tAsStatus(p.diagnose(pdb.Spec.MinAvailable, pdb.Status.CurrentHealthy)),\n\t\tToAge(pdb.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n\nfunc (PodDisruptionBudget) diagnose(v *intstr.IntOrString, healthy int32) error {\n\tif v == nil {\n\t\treturn nil\n\t}\n\tif v.IntVal > healthy {\n\t\treturn fmt.Errorf(\"expected %d but got %d\", v.IntVal, healthy)\n\t}\n\n\treturn nil\n}\n\n// Helpers...\n\nfunc numbToStr(n *intstr.IntOrString) string {\n\tif n == nil {\n\t\treturn NAValue\n\t}\n\tif n.Type == intstr.Int {\n\t\treturn strconv.Itoa(int(n.IntVal))\n\t}\n\treturn n.StrVal\n}\n"
  },
  {
    "path": "internal/render/pdb_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPodDisruptionBudgetRender(t *testing.T) {\n\tc := render.PodDisruptionBudget{}\n\tr := model1.NewRow(9)\n\n\trequire.NoError(t, c.Render(load(t, \"pdb\"), \"\", &r))\n\tassert.Equal(t, \"default/fred\", r.ID)\n\tassert.Equal(t, model1.Fields{\"default\", \"fred\", \"2\", render.NAValue, \"0\", \"0\", \"2\", \"0\"}, r.Fields[:8])\n}\n"
  },
  {
    "path": "internal/render/pod.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/api/resource\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n\t\"k8s.io/apimachinery/pkg/util/sets\"\n\tmv1beta1 \"k8s.io/metrics/pkg/apis/metrics/v1beta1\"\n)\n\nconst (\n\t// NodeUnreachablePodReason is reason and message set on a pod when its state\n\t// cannot be confirmed as kubelet is unresponsive on the node it is (was) running.\n\tNodeUnreachablePodReason = \"NodeLost\" // k8s.io/kubernetes/pkg/util/node.NodeUnreachablePodReason\n\tvulIdx                   = 2\n)\n\nconst (\n\tPhaseTerminating            = \"Terminating\"\n\tPhaseInitialized            = \"Initialized\"\n\tPhaseRunning                = \"Running\"\n\tPhaseNotReady               = \"NoReady\"\n\tPhaseCompleted              = \"Completed\"\n\tPhaseContainerCreating      = \"ContainerCreating\"\n\tPhasePodInitializing        = \"PodInitializing\"\n\tPhaseUnknown                = \"Unknown\"\n\tPhaseCrashLoop              = \"CrashLoopBackOff\"\n\tPhaseError                  = \"Error\"\n\tPhaseImagePullBackOff       = \"ImagePullBackOff\"\n\tPhaseOOMKilled              = \"OOMKilled\"\n\tPhasePending                = \"Pending\"\n\tPhaseContainerStatusUnknown = \"ContainerStatusUnknown\"\n\tPhaseEvicted                = \"Evicted\"\n)\n\nvar defaultPodHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"VS\", Attrs: model1.Attrs{VS: true}},\n\tmodel1.HeaderColumn{Name: \"PF\"},\n\tmodel1.HeaderColumn{Name: \"READY\"},\n\tmodel1.HeaderColumn{Name: \"STATUS\"},\n\tmodel1.HeaderColumn{Name: \"RESTARTS\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"LAST RESTART\", Attrs: model1.Attrs{Align: tview.AlignRight, Time: true, Wide: true}},\n\tmodel1.HeaderColumn{Name: \"CPU\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"CPU/RL\", Attrs: model1.Attrs{Align: tview.AlignRight, Wide: true}},\n\tmodel1.HeaderColumn{Name: \"%CPU/R\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"%CPU/L\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"MEM\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"MEM/RL\", Attrs: model1.Attrs{Align: tview.AlignRight, Wide: true}},\n\tmodel1.HeaderColumn{Name: \"%MEM/R\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"%MEM/L\", Attrs: model1.Attrs{Align: tview.AlignRight, MX: true}},\n\tmodel1.HeaderColumn{Name: \"GPU/RL\", Attrs: model1.Attrs{Align: tview.AlignRight, Wide: true}},\n\tmodel1.HeaderColumn{Name: \"IP\"},\n\tmodel1.HeaderColumn{Name: \"NODE\"},\n\tmodel1.HeaderColumn{Name: \"SERVICE-ACCOUNT\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"NOMINATED NODE\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"READINESS GATES\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"QOS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"LABELS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// Pod renders a K8s Pod to screen.\ntype Pod struct {\n\t*Base\n}\n\n// NewPod returns a new instance.\nfunc NewPod() *Pod {\n\treturn &Pod{\n\t\tBase: new(Base),\n\t}\n}\n\n// ColorerFunc colors a resource row.\nfunc (*Pod) ColorerFunc() model1.ColorerFunc {\n\treturn func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color {\n\t\tc := model1.DefaultColorer(ns, h, re)\n\n\t\tidx, ok := h.IndexOf(\"STATUS\", true)\n\t\tif !ok {\n\t\t\treturn c\n\t\t}\n\t\tstatus := strings.TrimSpace(re.Row.Fields[idx])\n\t\tswitch status {\n\t\tcase Pending, ContainerCreating:\n\t\t\tc = model1.PendingColor\n\t\tcase PodInitializing:\n\t\t\tc = model1.AddColor\n\t\tcase Initialized:\n\t\t\tc = model1.HighlightColor\n\t\tcase Completed:\n\t\t\tc = model1.CompletedColor\n\t\tcase Running:\n\t\t\tif c != model1.ErrColor {\n\t\t\t\tc = model1.StdColor\n\t\t\t}\n\t\tcase Terminating:\n\t\t\tc = model1.KillColor\n\t\t}\n\n\t\treturn c\n\t}\n}\n\n// Header returns a header row.\nfunc (p *Pod) Header(string) model1.Header {\n\treturn p.doHeader(defaultPodHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (p *Pod) Render(o any, _ string, row *model1.Row) error {\n\tpwm, ok := o.(*PodWithMetrics)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected PodWithMetrics, but got %T\", o)\n\t}\n\tif err := p.defaultRow(pwm, row); err != nil {\n\t\treturn err\n\t}\n\tif p.specs.isEmpty() {\n\t\treturn nil\n\t}\n\tcols, err := p.specs.realize(pwm.Raw.DeepCopy(), defaultPodHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\nfunc (p *Pod) defaultRow(pwm *PodWithMetrics, row *model1.Row) error {\n\tvar st v1.PodStatus\n\tif err := runtime.DefaultUnstructuredConverter.FromUnstructured(pwm.Raw.Object[\"status\"].(map[string]any), &st); err != nil {\n\t\treturn err\n\t}\n\tspec := new(v1.PodSpec)\n\tif err := runtime.DefaultUnstructuredConverter.FromUnstructured(pwm.Raw.Object[\"spec\"].(map[string]any), spec); err != nil {\n\t\treturn err\n\t}\n\n\tdt := pwm.Raw.GetDeletionTimestamp()\n\tcReady, _, cRestarts, lastRestart := p.ContainerStats(st.ContainerStatuses)\n\n\tiReady, iTerminated, iRestarts := p.initContainerStats(spec.InitContainers, st.InitContainerStatuses)\n\tcReady += iReady\n\tallCounts := len(spec.Containers) + iTerminated\n\trgr, rgt := p.readinessGateStats(spec, &st)\n\tready := hasPodReadyCondition(st.Conditions)\n\n\tvar ccmx []mv1beta1.ContainerMetrics\n\tif pwm.MX != nil {\n\t\tccmx = pwm.MX.Containers\n\t}\n\tc, r := gatherPodMX(spec, ccmx)\n\tphase := p.Phase(dt, spec, &st)\n\n\tns, n := pwm.Raw.GetNamespace(), pwm.Raw.GetName()\n\n\trow.ID = client.FQN(ns, n)\n\trow.Fields = model1.Fields{\n\t\tns,\n\t\tn,\n\t\tcomputeVulScore(ns, pwm.Raw.GetLabels(), spec),\n\t\t\"●\",\n\t\tstrconv.Itoa(cReady) + \"/\" + strconv.Itoa(allCounts),\n\t\tphase,\n\t\tstrconv.Itoa(cRestarts + iRestarts),\n\t\tToAge(lastRestart),\n\t\ttoMc(c.cpu),\n\t\ttoMc(r.cpu) + \":\" + toMc(r.lcpu),\n\t\tclient.ToPercentageStr(c.cpu, r.cpu),\n\t\tclient.ToPercentageStr(c.cpu, r.lcpu),\n\t\ttoMi(c.mem),\n\t\ttoMi(r.mem) + \":\" + toMi(r.lmem),\n\t\tclient.ToPercentageStr(c.mem, r.mem),\n\t\tclient.ToPercentageStr(c.mem, r.lmem),\n\t\ttoMc(r.gpu) + \":\" + toMc(r.lgpu),\n\t\tna(st.PodIP),\n\t\tna(spec.NodeName),\n\t\tna(spec.ServiceAccountName),\n\t\tasNominated(st.NominatedNodeName),\n\t\tasReadinessGate(spec, &st),\n\t\tp.mapQOS(st.QOSClass),\n\t\tmapToStr(pwm.Raw.GetLabels()),\n\t\tAsStatus(p.diagnose(phase, cReady, allCounts, ready, rgr, rgt)),\n\t\tToAge(pwm.Raw.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n\n// Healthy checks component health.\nfunc (p *Pod) Healthy(_ context.Context, o any) error {\n\tpwm, ok := o.(*PodWithMetrics)\n\tif !ok {\n\t\tslog.Error(\"Expected *PodWithMetrics\", slogs.Type, fmt.Sprintf(\"%T\", o))\n\t\treturn nil\n\t}\n\tvar st v1.PodStatus\n\tif err := runtime.DefaultUnstructuredConverter.FromUnstructured(pwm.Raw.Object[\"status\"].(map[string]any), &st); err != nil {\n\t\tslog.Error(\"Failed to convert unstructured to PodState\", slogs.Error, err)\n\t\treturn nil\n\t}\n\tspec := new(v1.PodSpec)\n\tif err := runtime.DefaultUnstructuredConverter.FromUnstructured(pwm.Raw.Object[\"spec\"].(map[string]any), spec); err != nil {\n\t\tslog.Error(\"Failed to convert unstructured to PodSpec\", slogs.Error, err)\n\t\treturn nil\n\t}\n\tdt := pwm.Raw.GetDeletionTimestamp()\n\tphase := p.Phase(dt, spec, &st)\n\tcr, _, _, _ := p.ContainerStats(st.ContainerStatuses)\n\tct := len(st.ContainerStatuses)\n\n\ticr, ict, _ := p.initContainerStats(spec.InitContainers, st.InitContainerStatuses)\n\tcr += icr\n\tct += ict\n\n\tready := hasPodReadyCondition(st.Conditions)\n\trgr, rgt := p.readinessGateStats(spec, &st)\n\n\treturn p.diagnose(phase, cr, ct, ready, rgr, rgt)\n}\n\nfunc (*Pod) diagnose(phase string, cr, ct int, ready bool, rgr, rgt int) error {\n\tif phase == Completed {\n\t\treturn nil\n\t}\n\tif cr != ct || ct == 0 {\n\t\treturn fmt.Errorf(\"container ready check failed: %d of %d\", cr, ct)\n\t}\n\tif rgt > 0 && rgr != rgt {\n\t\treturn fmt.Errorf(\"readiness gate check failed: %d of %d\", rgr, rgt)\n\t}\n\tif !ready {\n\t\treturn fmt.Errorf(\"pod condition ready is false\")\n\t}\n\tif phase == Terminating {\n\t\treturn fmt.Errorf(\"pod is terminating\")\n\t}\n\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc asNominated(n string) string {\n\tif n == \"\" {\n\t\treturn MissingValue\n\t}\n\treturn n\n}\n\nfunc asReadinessGate(spec *v1.PodSpec, st *v1.PodStatus) string {\n\tif len(spec.ReadinessGates) == 0 {\n\t\treturn MissingValue\n\t}\n\n\tvar trueConditions int\n\tfor _, readinessGate := range spec.ReadinessGates {\n\t\tconditionType := readinessGate.ConditionType\n\t\tfor _, condition := range st.Conditions {\n\t\t\tif condition.Type == conditionType {\n\t\t\t\tif condition.Status == \"True\" {\n\t\t\t\t\ttrueConditions++\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn strconv.Itoa(trueConditions) + \"/\" + strconv.Itoa(len(spec.ReadinessGates))\n}\n\n// PodWithMetrics represents a pod and its metrics.\ntype PodWithMetrics struct {\n\tRaw *unstructured.Unstructured\n\tMX  *mv1beta1.PodMetrics\n}\n\n// GetObjectKind returns a schema object.\nfunc (*PodWithMetrics) GetObjectKind() schema.ObjectKind {\n\treturn nil\n}\n\n// DeepCopyObject returns a container copy.\nfunc (p *PodWithMetrics) DeepCopyObject() runtime.Object {\n\treturn p\n}\n\nfunc gatherPodMX(spec *v1.PodSpec, ccmx []mv1beta1.ContainerMetrics) (c, r metric) {\n\tcc := make([]v1.Container, 0, len(spec.InitContainers)+len(spec.Containers))\n\tcc = append(cc, filterSidecarCO(spec.InitContainers)...)\n\tcc = append(cc, spec.Containers...)\n\n\trcpu, rmem, rgpu := cosRequests(cc)\n\tr.cpu, r.mem, r.gpu = rcpu.MilliValue(), rmem.Value(), rgpu.Value()\n\n\tlcpu, lmem, lgpu := cosLimits(cc)\n\tr.lcpu, r.lmem, r.lgpu = lcpu.MilliValue(), lmem.Value(), lgpu.Value()\n\n\tccpu, cmem := currentRes(ccmx)\n\tc.cpu, c.mem = ccpu.MilliValue(), cmem.Value()\n\n\treturn\n}\n\nfunc cosLimits(cc []v1.Container) (cpuQ, memQ, gpuQ *resource.Quantity) {\n\tcpuQ, gpuQ, memQ = new(resource.Quantity), new(resource.Quantity), new(resource.Quantity)\n\tfor i := range cc {\n\t\tlimits := cc[i].Resources.Limits\n\t\tif len(limits) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif q := limits.Cpu(); q != nil {\n\t\t\tcpuQ.Add(*q)\n\t\t}\n\t\tif q := limits.Memory(); q != nil {\n\t\t\tmemQ.Add(*q)\n\t\t}\n\t\tif q := extractGPU(limits); q != nil {\n\t\t\tgpuQ.Add(*q)\n\t\t}\n\t}\n\n\treturn\n}\n\nfunc cosRequests(cc []v1.Container) (cpuQ, memQ, gpuQ *resource.Quantity) {\n\tcpuQ, gpuQ, memQ = new(resource.Quantity), new(resource.Quantity), new(resource.Quantity)\n\tfor i := range cc {\n\t\tco := cc[i]\n\t\trl := containerRequests(&co)\n\t\tif q := rl.Cpu(); q != nil {\n\t\t\tcpuQ.Add(*q)\n\t\t}\n\t\tif q := rl.Memory(); q != nil {\n\t\t\tmemQ.Add(*q)\n\t\t}\n\t\tif q := extractGPU(rl); q != nil {\n\t\t\tgpuQ.Add(*q)\n\t\t}\n\t}\n\n\treturn\n}\n\nfunc extractGPU(rl v1.ResourceList) *resource.Quantity {\n\tfor _, v := range config.KnownGPUVendors {\n\t\tif q, ok := rl[v1.ResourceName(v)]; ok {\n\t\t\treturn &q\n\t\t}\n\t}\n\n\treturn &resource.Quantity{Format: resource.DecimalSI}\n}\n\nfunc currentRes(ccmx []mv1beta1.ContainerMetrics) (cpuQ, memQ *resource.Quantity) {\n\tcpuQ = new(resource.Quantity)\n\tmemQ = new(resource.Quantity)\n\tif ccmx == nil {\n\t\treturn\n\t}\n\tfor _, co := range ccmx {\n\t\tc, m := co.Usage.Cpu(), co.Usage.Memory()\n\t\tcpuQ.Add(*c)\n\t\tmemQ.Add(*m)\n\t}\n\n\treturn\n}\n\nfunc (*Pod) mapQOS(class v1.PodQOSClass) string {\n\t//nolint:exhaustive\n\tswitch class {\n\tcase v1.PodQOSGuaranteed:\n\t\treturn \"GA\"\n\tcase v1.PodQOSBurstable:\n\t\treturn \"BU\"\n\tdefault:\n\t\treturn \"BE\"\n\t}\n}\n\n// ContainerStats reports pod container stats.\nfunc (*Pod) ContainerStats(cc []v1.ContainerStatus) (readyCnt, terminatedCnt, restartCnt int, latest metav1.Time) {\n\tfor i := range cc {\n\t\tif cc[i].State.Terminated != nil {\n\t\t\tterminatedCnt++\n\t\t}\n\t\tif cc[i].Ready {\n\t\t\treadyCnt++\n\t\t}\n\t\trestartCnt += int(cc[i].RestartCount)\n\n\t\tif t := cc[i].LastTerminationState.Terminated; t != nil {\n\t\t\tts := cc[i].LastTerminationState.Terminated.FinishedAt\n\t\t\tif latest.IsZero() || ts.After(latest.Time) {\n\t\t\t\tlatest = ts\n\t\t\t}\n\t\t}\n\t}\n\n\treturn\n}\n\nfunc (*Pod) initContainerStats(cc []v1.Container, cos []v1.ContainerStatus) (ready, total, restart int) {\n\tfor i := range cos {\n\t\tif !isSideCarContainer(cc[i].RestartPolicy) {\n\t\t\tcontinue\n\t\t}\n\t\ttotal++\n\t\tif cos[i].Ready {\n\t\t\tready++\n\t\t}\n\t\trestart += int(cos[i].RestartCount)\n\t}\n\treturn\n}\n\nfunc (*Pod) readinessGateStats(spec *v1.PodSpec, st *v1.PodStatus) (ready, total int) {\n\ttotal = len(spec.ReadinessGates)\n\tfor _, readinessGate := range spec.ReadinessGates {\n\t\tfor _, condition := range st.Conditions {\n\t\t\tif condition.Type == readinessGate.ConditionType {\n\t\t\t\tif condition.Status == \"True\" {\n\t\t\t\t\tready++\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\n// Phase reports the given pod phase.\nfunc (p *Pod) Phase(dt *metav1.Time, spec *v1.PodSpec, st *v1.PodStatus) string {\n\tstatus := string(st.Phase)\n\tif st.Reason != \"\" {\n\t\tif dt != nil && st.Reason == NodeUnreachablePodReason {\n\t\t\treturn \"Unknown\"\n\t\t}\n\t\tstatus = st.Reason\n\t}\n\n\tstatus, ok := p.initContainerPhase(spec, st, status)\n\tif ok {\n\t\treturn status\n\t}\n\n\tstatus, ok = p.containerPhase(st, status)\n\tif ok && status == Completed {\n\t\tstatus = Running\n\t}\n\tif dt == nil {\n\t\treturn status\n\t}\n\n\treturn Terminating\n}\n\nfunc (*Pod) containerPhase(st *v1.PodStatus, status string) (string, bool) {\n\tvar running bool\n\tfor i := len(st.ContainerStatuses) - 1; i >= 0; i-- {\n\t\tcs := st.ContainerStatuses[i]\n\t\tswitch {\n\t\tcase cs.State.Waiting != nil && cs.State.Waiting.Reason != \"\":\n\t\t\tstatus = cs.State.Waiting.Reason\n\t\tcase cs.State.Terminated != nil && cs.State.Terminated.Reason != \"\":\n\t\t\tstatus = cs.State.Terminated.Reason\n\t\tcase cs.State.Terminated != nil:\n\t\t\tif cs.State.Terminated.Signal != 0 {\n\t\t\t\tstatus = \"Signal:\" + strconv.Itoa(int(cs.State.Terminated.Signal))\n\t\t\t} else {\n\t\t\t\tstatus = \"ExitCode:\" + strconv.Itoa(int(cs.State.Terminated.ExitCode))\n\t\t\t}\n\t\tcase cs.Ready && cs.State.Running != nil:\n\t\t\trunning = true\n\t\t}\n\t}\n\n\treturn status, running\n}\n\nfunc (*Pod) initContainerPhase(spec *v1.PodSpec, pst *v1.PodStatus, status string) (string, bool) {\n\tcount := len(spec.InitContainers)\n\tsidecars := sets.New[string]()\n\tfor i := range spec.InitContainers {\n\t\tco := spec.InitContainers[i]\n\t\tif isSideCarContainer(co.RestartPolicy) {\n\t\t\tsidecars.Insert(co.Name)\n\t\t}\n\t}\n\tfor i := range pst.InitContainerStatuses {\n\t\tif s := checkInitContainerStatus(&pst.InitContainerStatuses[i], i, count, sidecars.Has(pst.InitContainerStatuses[i].Name)); s != \"\" {\n\t\t\treturn s, true\n\t\t}\n\t}\n\n\treturn status, false\n}\n\n// ----------------------------------------------------------------------------\n// Helpers..\n\nfunc checkInitContainerStatus(cs *v1.ContainerStatus, count, initCount int, restartable bool) string {\n\tswitch {\n\tcase cs.State.Terminated != nil:\n\t\tif cs.State.Terminated.ExitCode == 0 {\n\t\t\treturn \"\"\n\t\t}\n\t\tif cs.State.Terminated.Reason != \"\" {\n\t\t\treturn \"Init:\" + cs.State.Terminated.Reason\n\t\t}\n\t\tif cs.State.Terminated.Signal != 0 {\n\t\t\treturn \"Init:Signal:\" + strconv.Itoa(int(cs.State.Terminated.Signal))\n\t\t}\n\t\treturn \"Init:ExitCode:\" + strconv.Itoa(int(cs.State.Terminated.ExitCode))\n\tcase restartable && cs.Started != nil && *cs.Started:\n\t\tif cs.Ready {\n\t\t\treturn \"\"\n\t\t}\n\tcase cs.State.Waiting != nil && cs.State.Waiting.Reason != \"\" && cs.State.Waiting.Reason != \"PodInitializing\":\n\t\treturn \"Init:\" + cs.State.Waiting.Reason\n\t}\n\n\treturn \"Init:\" + strconv.Itoa(count) + \"/\" + strconv.Itoa(initCount)\n}\n\n// PodStatus computes pod status.\nfunc PodStatus(pod *v1.Pod) string {\n\treason := string(pod.Status.Phase)\n\tif pod.Status.Reason != \"\" {\n\t\treason = pod.Status.Reason\n\t}\n\n\tfor _, condition := range pod.Status.Conditions {\n\t\tif condition.Type == v1.PodScheduled && condition.Reason == v1.PodReasonSchedulingGated {\n\t\t\treason = v1.PodReasonSchedulingGated\n\t\t}\n\t}\n\n\tvar initializing bool\n\tfor i := range pod.Status.InitContainerStatuses {\n\t\tcontainer := pod.Status.InitContainerStatuses[i]\n\t\tswitch {\n\t\tcase container.State.Terminated != nil && container.State.Terminated.ExitCode == 0:\n\t\t\tcontinue\n\t\tcase container.State.Terminated != nil:\n\t\t\tif container.State.Terminated.Reason == \"\" {\n\t\t\t\tif container.State.Terminated.Signal != 0 {\n\t\t\t\t\treason = fmt.Sprintf(\"Init:Signal:%d\", container.State.Terminated.Signal)\n\t\t\t\t} else {\n\t\t\t\t\treason = fmt.Sprintf(\"Init:ExitCode:%d\", container.State.Terminated.ExitCode)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treason = \"Init:\" + container.State.Terminated.Reason\n\t\t\t}\n\t\t\tinitializing = true\n\t\tcase container.State.Waiting != nil && container.State.Waiting.Reason != \"\" && container.State.Waiting.Reason != \"PodInitializing\":\n\t\t\treason = \"Init:\" + container.State.Waiting.Reason\n\t\t\tinitializing = true\n\t\tdefault:\n\t\t\treason = fmt.Sprintf(\"Init:%d/%d\", i, len(pod.Spec.InitContainers))\n\t\t\tinitializing = true\n\t\t}\n\t\tbreak\n\t}\n\tif !initializing {\n\t\tvar hasRunning bool\n\t\tfor i := len(pod.Status.ContainerStatuses) - 1; i >= 0; i-- {\n\t\t\tcontainer := pod.Status.ContainerStatuses[i]\n\t\t\tswitch {\n\t\t\tcase container.State.Waiting != nil && container.State.Waiting.Reason != \"\":\n\t\t\t\treason = container.State.Waiting.Reason\n\t\t\tcase container.State.Terminated != nil && container.State.Terminated.Reason != \"\":\n\t\t\t\treason = container.State.Terminated.Reason\n\t\t\tcase container.State.Terminated != nil && container.State.Terminated.Reason == \"\":\n\t\t\t\tif container.State.Terminated.Signal != 0 {\n\t\t\t\t\treason = fmt.Sprintf(\"Signal:%d\", container.State.Terminated.Signal)\n\t\t\t\t} else {\n\t\t\t\t\treason = fmt.Sprintf(\"ExitCode:%d\", container.State.Terminated.ExitCode)\n\t\t\t\t}\n\t\t\tcase container.Ready && container.State.Running != nil:\n\t\t\t\thasRunning = true\n\t\t\t}\n\t\t}\n\n\t\tif reason == PhaseCompleted && hasRunning {\n\t\t\tif hasPodReadyCondition(pod.Status.Conditions) {\n\t\t\t\treason = PhaseRunning\n\t\t\t} else {\n\t\t\t\treason = PhaseNotReady\n\t\t\t}\n\t\t}\n\t}\n\n\tif pod.DeletionTimestamp != nil && pod.Status.Reason == NodeUnreachablePodReason {\n\t\treason = PhaseUnknown\n\t} else if pod.DeletionTimestamp != nil {\n\t\treason = PhaseTerminating\n\t}\n\n\treturn reason\n}\n\nfunc hasPodReadyCondition(conditions []v1.PodCondition) bool {\n\tfor _, condition := range conditions {\n\t\tif condition.Type == v1.PodReady && condition.Status == v1.ConditionTrue {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc isSideCarContainer(p *v1.ContainerRestartPolicy) bool {\n\treturn p != nil && *p == v1.ContainerRestartPolicyAlways\n}\n\nfunc filterSidecarCO(cc []v1.Container) []v1.Container {\n\trcc := make([]v1.Container, 0, len(cc))\n\tfor i := range cc {\n\t\tif isSideCarContainer(cc[i].RestartPolicy) {\n\t\t\trcc = append(rcc, cc[i])\n\t\t}\n\t}\n\n\treturn rcc\n}\n"
  },
  {
    "path": "internal/render/pod_int_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\tv1 \"k8s.io/api/core/v1\"\n\tres \"k8s.io/apimachinery/pkg/api/resource\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\tmv1beta1 \"k8s.io/metrics/pkg/apis/metrics/v1beta1\"\n)\n\nfunc Test_checkInitContainerStatus(t *testing.T) {\n\ttrueVal := true\n\tuu := map[string]struct {\n\t\tstatus       v1.ContainerStatus\n\t\te            string\n\t\tcount, total int\n\t\trestart      bool\n\t}{\n\t\t\"none\": {\n\t\t\te: \"Init:0/0\",\n\t\t},\n\n\t\t\"restart\": {\n\t\t\tstatus: v1.ContainerStatus{\n\t\t\t\tName:    \"ic1\",\n\t\t\t\tStarted: &trueVal,\n\t\t\t\tState:   v1.ContainerState{},\n\t\t\t},\n\t\t\trestart: true,\n\t\t\te:       \"Init:0/0\",\n\t\t},\n\n\t\t\"no-restart\": {\n\t\t\tstatus: v1.ContainerStatus{\n\t\t\t\tName:    \"ic1\",\n\t\t\t\tStarted: &trueVal,\n\t\t\t\tState:   v1.ContainerState{},\n\t\t\t},\n\t\t\te: \"Init:0/0\",\n\t\t},\n\n\t\t\"terminated-reason\": {\n\t\t\tstatus: v1.ContainerStatus{\n\t\t\t\tName: \"ic1\",\n\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\tTerminated: &v1.ContainerStateTerminated{\n\t\t\t\t\t\tExitCode: 1,\n\t\t\t\t\t\tReason:   \"blah\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"Init:blah\",\n\t\t},\n\n\t\t\"terminated-signal\": {\n\t\t\tstatus: v1.ContainerStatus{\n\t\t\t\tName: \"ic1\",\n\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\tTerminated: &v1.ContainerStateTerminated{\n\t\t\t\t\t\tExitCode: 1,\n\t\t\t\t\t\tSignal:   9,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"Init:Signal:9\",\n\t\t},\n\n\t\t\"terminated-code\": {\n\t\t\tstatus: v1.ContainerStatus{\n\t\t\t\tName: \"ic1\",\n\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\tTerminated: &v1.ContainerStateTerminated{\n\t\t\t\t\t\tExitCode: 1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"Init:ExitCode:1\",\n\t\t},\n\n\t\t\"terminated-restart\": {\n\t\t\tstatus: v1.ContainerStatus{\n\t\t\t\tName: \"ic1\",\n\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\tTerminated: &v1.ContainerStateTerminated{\n\t\t\t\t\t\tReason: \"blah\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"waiting\": {\n\t\t\tstatus: v1.ContainerStatus{\n\t\t\t\tName: \"ic1\",\n\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\tWaiting: &v1.ContainerStateWaiting{\n\t\t\t\t\t\tReason: \"blah\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"Init:blah\",\n\t\t},\n\n\t\t\"waiting-init\": {\n\t\t\tstatus: v1.ContainerStatus{\n\t\t\t\tName: \"ic1\",\n\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\tWaiting: &v1.ContainerStateWaiting{\n\t\t\t\t\t\tReason: \"PodInitializing\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"Init:0/0\",\n\t\t},\n\n\t\t\"running\": {\n\t\t\tstatus: v1.ContainerStatus{\n\t\t\t\tName: \"ic1\",\n\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\tRunning: &v1.ContainerStateRunning{},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"Init:0/0\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, checkInitContainerStatus(&u.status, u.count, u.total, u.restart))\n\t\t})\n\t}\n}\n\nfunc Test_containerPhase(t *testing.T) {\n\tuu := map[string]struct {\n\t\tstatus v1.PodStatus\n\t\te      string\n\t\tok     bool\n\t}{\n\t\t\"none\": {},\n\n\t\t\"empty\": {\n\t\t\tstatus: v1.PodStatus{\n\t\t\t\tPhase: PhaseUnknown,\n\t\t\t},\n\t\t},\n\n\t\t\"waiting\": {\n\t\t\tstatus: v1.PodStatus{\n\t\t\t\tPhase: PhaseUnknown,\n\t\t\t\tInitContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"ic1\",\n\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\tRunning: &v1.ContainerStateRunning{},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"c1\",\n\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\tWaiting: &v1.ContainerStateWaiting{\n\t\t\t\t\t\t\t\tReason: \"waiting\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"waiting\",\n\t\t},\n\n\t\t\"terminated\": {\n\t\t\tstatus: v1.PodStatus{\n\t\t\t\tPhase: PhaseUnknown,\n\t\t\t\tInitContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"ic1\",\n\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\tRunning: &v1.ContainerStateRunning{},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"c1\",\n\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\tTerminated: &v1.ContainerStateTerminated{\n\t\t\t\t\t\t\t\tReason: \"done\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"done\",\n\t\t},\n\n\t\t\"terminated-sig\": {\n\t\t\tstatus: v1.PodStatus{\n\t\t\t\tPhase: PhaseUnknown,\n\t\t\t\tInitContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"ic1\",\n\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\tRunning: &v1.ContainerStateRunning{},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"c1\",\n\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\tTerminated: &v1.ContainerStateTerminated{\n\t\t\t\t\t\t\t\tSignal: 9,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"Signal:9\",\n\t\t},\n\n\t\t\"terminated-code\": {\n\t\t\tstatus: v1.PodStatus{\n\t\t\t\tPhase: PhaseUnknown,\n\t\t\t\tInitContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"ic1\",\n\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\tRunning: &v1.ContainerStateRunning{},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"c1\",\n\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\tTerminated: &v1.ContainerStateTerminated{\n\t\t\t\t\t\t\t\tExitCode: 2,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"ExitCode:2\",\n\t\t},\n\n\t\t\"running\": {\n\t\t\tstatus: v1.PodStatus{\n\t\t\t\tPhase: PhaseUnknown,\n\t\t\t\tInitContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tName: \"ic1\",\n\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\tRunning: &v1.ContainerStateRunning{},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:  \"c1\",\n\t\t\t\t\t\tReady: true,\n\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\tRunning: &v1.ContainerStateRunning{},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tok: true,\n\t\t},\n\t}\n\n\tvar p Pod\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\ts, ok := p.containerPhase(&u.status, \"\")\n\t\t\tassert.Equal(t, u.ok, ok)\n\t\t\tassert.Equal(t, u.e, s)\n\t\t})\n\t}\n}\n\nfunc Test_isSideCarContainer(t *testing.T) {\n\talways, never := v1.ContainerRestartPolicyAlways, v1.ContainerRestartPolicy(\"never\")\n\tuu := map[string]struct {\n\t\tp *v1.ContainerRestartPolicy\n\t\te bool\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"sidecar\": {\n\t\t\tp: &always,\n\t\t\te: true,\n\t\t},\n\n\t\t\"no-sidecar\": {\n\t\t\tp: &never,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, isSideCarContainer(u.p))\n\t\t})\n\t}\n}\n\nfunc Test_filterSidecarCO(t *testing.T) {\n\talways := v1.ContainerRestartPolicyAlways\n\n\tuu := map[string]struct {\n\t\tcc, ecc []v1.Container\n\t}{\n\t\t\"empty\": {\n\t\t\tcc:  []v1.Container{},\n\t\t\tecc: []v1.Container{},\n\t\t},\n\n\t\t\"restartable\": {\n\t\t\tcc: []v1.Container{\n\t\t\t\t{\n\t\t\t\t\tName:          \"c1\",\n\t\t\t\t\tRestartPolicy: &always,\n\t\t\t\t},\n\t\t\t},\n\t\t\tecc: []v1.Container{\n\t\t\t\t{\n\t\t\t\t\tName:          \"c1\",\n\t\t\t\t\tRestartPolicy: &always,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\n\t\t\"not-restartable\": {\n\t\t\tcc: []v1.Container{\n\t\t\t\t{\n\t\t\t\t\tName: \"c1\",\n\t\t\t\t},\n\t\t\t},\n\t\t\tecc: []v1.Container{},\n\t\t},\n\n\t\t\"mixed\": {\n\t\t\tcc: []v1.Container{\n\t\t\t\t{\n\t\t\t\t\tName: \"c1\",\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName:          \"c2\",\n\t\t\t\t\tRestartPolicy: &always,\n\t\t\t\t},\n\t\t\t},\n\t\t\tecc: []v1.Container{\n\t\t\t\t{\n\t\t\t\t\tName:          \"c2\",\n\t\t\t\t\tRestartPolicy: &always,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.ecc, filterSidecarCO(u.cc))\n\t\t})\n\t}\n}\n\nfunc Test_lastRestart(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcontainerStatuses []v1.ContainerStatus\n\t\texpected          metav1.Time\n\t}{\n\t\t\"no-restarts\": {\n\t\t\tcontainerStatuses: []v1.ContainerStatus{\n\t\t\t\t{\n\t\t\t\t\tName:                 \"c1\",\n\t\t\t\t\tLastTerminationState: v1.ContainerState{},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: metav1.Time{},\n\t\t},\n\t\t\"single-container-restart\": {\n\t\t\tcontainerStatuses: []v1.ContainerStatus{\n\t\t\t\t{\n\t\t\t\t\tName: \"c1\",\n\t\t\t\t\tLastTerminationState: v1.ContainerState{\n\t\t\t\t\t\tTerminated: &v1.ContainerStateTerminated{\n\t\t\t\t\t\t\tFinishedAt: metav1.Time{Time: testTime()},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: metav1.Time{Time: testTime()},\n\t\t},\n\t\t\"multiple-container-restarts\": {\n\t\t\tcontainerStatuses: []v1.ContainerStatus{\n\t\t\t\t{\n\t\t\t\t\tName: \"c1\",\n\t\t\t\t\tLastTerminationState: v1.ContainerState{\n\t\t\t\t\t\tTerminated: &v1.ContainerStateTerminated{\n\t\t\t\t\t\t\tFinishedAt: metav1.Time{Time: testTime().Add(-1 * time.Hour)},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName: \"c2\",\n\t\t\t\t\tLastTerminationState: v1.ContainerState{\n\t\t\t\t\t\tTerminated: &v1.ContainerStateTerminated{\n\t\t\t\t\t\t\tFinishedAt: metav1.Time{Time: testTime()},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: metav1.Time{Time: testTime()},\n\t\t},\n\t\t\"mixed-termination-states\": {\n\t\t\tcontainerStatuses: []v1.ContainerStatus{\n\t\t\t\t{\n\t\t\t\t\tName:                 \"c1\",\n\t\t\t\t\tLastTerminationState: v1.ContainerState{},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tName: \"c2\",\n\t\t\t\t\tLastTerminationState: v1.ContainerState{\n\t\t\t\t\t\tTerminated: &v1.ContainerStateTerminated{\n\t\t\t\t\t\t\tFinishedAt: metav1.Time{Time: testTime()},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpected: metav1.Time{Time: testTime()},\n\t\t},\n\t}\n\n\tvar p Pod\n\tfor name, u := range uu {\n\t\tt.Run(name, func(t *testing.T) {\n\t\t\t_, _, _, lr := p.ContainerStats(u.containerStatuses)\n\t\t\tassert.Equal(t, u.expected, lr)\n\t\t})\n\t}\n}\n\nfunc Test_gatherPodMX(t *testing.T) {\n\tuu := map[string]struct {\n\t\tspec *v1.PodSpec\n\t\tmx   []mv1beta1.ContainerMetrics\n\t\tc, r metric\n\t\tperc string\n\t}{\n\t\t\"single\": {\n\t\t\tspec: &v1.PodSpec{\n\t\t\t\tContainers: []v1.Container{\n\t\t\t\t\tmakeContainer(\"c1\", false, \"10m\", \"1Mi\", \"20m\", \"2Mi\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tmx: []mv1beta1.ContainerMetrics{\n\t\t\t\tmakeCoMX(\"c1\", \"1m\", \"22Mi\"),\n\t\t\t},\n\t\t\tc: metric{\n\t\t\t\tcpu: 1,\n\t\t\t\tmem: 22 * client.MegaByte,\n\t\t\t\tgpu: 1,\n\t\t\t},\n\t\t\tr: metric{\n\t\t\t\tcpu:  10,\n\t\t\t\tmem:  1 * client.MegaByte,\n\t\t\t\tgpu:  1,\n\t\t\t\tlcpu: 20,\n\t\t\t\tlmem: 2 * client.MegaByte,\n\t\t\t\tlgpu: 1,\n\t\t\t},\n\t\t\tperc: \"10\",\n\t\t},\n\n\t\t\"multi\": {\n\t\t\tspec: &v1.PodSpec{\n\t\t\t\tContainers: []v1.Container{\n\t\t\t\t\tmakeContainer(\"c1\", false, \"11m\", \"22Mi\", \"111m\", \"44Mi\"),\n\t\t\t\t\tmakeContainer(\"c2\", false, \"93m\", \"1402Mi\", \"0m\", \"2804Mi\"),\n\t\t\t\t\tmakeContainer(\"c3\", false, \"11m\", \"34Mi\", \"0m\", \"69Mi\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tr: metric{\n\t\t\t\tcpu:  11 + 93 + 11,\n\t\t\t\tgpu:  1,\n\t\t\t\tmem:  (22 + 1402 + 34) * client.MegaByte,\n\t\t\t\tlcpu: 111 + 0 + 0,\n\t\t\t\tlgpu: 1,\n\t\t\t\tlmem: (44 + 2804 + 69) * client.MegaByte,\n\t\t\t},\n\t\t\tmx: []mv1beta1.ContainerMetrics{\n\t\t\t\tmakeCoMX(\"c1\", \"1m\", \"22Mi\"),\n\t\t\t\tmakeCoMX(\"c2\", \"51m\", \"1275Mi\"),\n\t\t\t\tmakeCoMX(\"c3\", \"1m\", \"27Mi\"),\n\t\t\t},\n\t\t\tc: metric{\n\t\t\t\tcpu: 1 + 51 + 1,\n\t\t\t\tgpu: 1,\n\t\t\t\tmem: (22 + 1275 + 27) * client.MegaByte,\n\t\t\t},\n\t\t\tperc: \"46\",\n\t\t},\n\n\t\t\"sidecar\": {\n\t\t\tspec: &v1.PodSpec{\n\t\t\t\tContainers: []v1.Container{\n\t\t\t\t\tmakeContainer(\"c1\", false, \"11m\", \"22Mi\", \"111m\", \"44Mi\"),\n\t\t\t\t},\n\t\t\t\tInitContainers: []v1.Container{\n\t\t\t\t\tmakeContainer(\"c2\", true, \"93m\", \"1402Mi\", \"0m\", \"2804Mi\"),\n\t\t\t\t},\n\t\t\t},\n\t\t\tr: metric{\n\t\t\t\tcpu:  11 + 93,\n\t\t\t\tgpu:  1,\n\t\t\t\tmem:  (22 + 1402) * client.MegaByte,\n\t\t\t\tlcpu: 111 + 0,\n\t\t\t\tlgpu: 1,\n\t\t\t\tlmem: (44 + 2804) * client.MegaByte,\n\t\t\t},\n\t\t\tmx: []mv1beta1.ContainerMetrics{\n\t\t\t\tmakeCoMX(\"c1\", \"1m\", \"22Mi\"),\n\t\t\t\tmakeCoMX(\"c2\", \"51m\", \"1275Mi\"),\n\t\t\t},\n\t\t\tc: metric{\n\t\t\t\tcpu: 1 + 51,\n\t\t\t\tgpu: 1,\n\t\t\t\tmem: (22 + 1275) * client.MegaByte,\n\t\t\t},\n\t\t\tperc: \"50\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tc, r := gatherPodMX(u.spec, u.mx)\n\t\t\tassert.Equal(t, u.c.cpu, c.cpu)\n\t\t\tassert.Equal(t, u.c.mem, c.mem)\n\t\t\tassert.Equal(t, u.c.lcpu, c.lcpu)\n\t\t\tassert.Equal(t, u.c.lmem, c.lmem)\n\t\t\tassert.Equal(t, u.c.lgpu, c.lgpu)\n\n\t\t\tassert.Equal(t, u.r.cpu, r.cpu)\n\t\t\tassert.Equal(t, u.r.mem, r.mem)\n\t\t\tassert.Equal(t, u.r.lcpu, r.lcpu)\n\t\t\tassert.Equal(t, u.r.lmem, r.lmem)\n\t\t\tassert.Equal(t, u.r.gpu, r.gpu)\n\t\t\tassert.Equal(t, u.r.lgpu, r.lgpu)\n\n\t\t\tassert.Equal(t, u.perc, client.ToPercentageStr(c.cpu, r.cpu))\n\t\t})\n\t}\n}\n\nfunc Test_podLimits(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcc []v1.Container\n\t\tl  v1.ResourceList\n\t}{\n\t\t\"plain\": {\n\t\t\tcc: []v1.Container{\n\t\t\t\tmakeContainer(\"c1\", false, \"10m\", \"1Mi\", \"20m\", \"2Mi\"),\n\t\t\t},\n\t\t\tl: makeRes(\"20m\", \"2Mi\"),\n\t\t},\n\t\t\"multi-co\": {\n\t\t\tcc: []v1.Container{\n\t\t\t\tmakeContainer(\"c1\", false, \"10m\", \"1Mi\", \"20m\", \"2Mi\"),\n\t\t\t\tmakeContainer(\"c2\", false, \"10m\", \"1Mi\", \"40m\", \"4Mi\"),\n\t\t\t},\n\t\t\tl: makeRes(\"60m\", \"6Mi\"),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tc, m, g := cosLimits(u.cc)\n\t\t\tassert.True(t, c.Equal(*u.l.Cpu()))\n\t\t\tassert.True(t, m.Equal(*u.l.Memory()))\n\t\t\tassert.True(t, g.Equal(*extractGPU(u.l)))\n\t\t})\n\t}\n}\n\nfunc Test_podRequests(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcc []v1.Container\n\t\te  v1.ResourceList\n\t}{\n\t\t\"plain\": {\n\t\t\tcc: []v1.Container{\n\t\t\t\tmakeContainer(\"c1\", false, \"10m\", \"1Mi\", \"20m\", \"2Mi\"),\n\t\t\t},\n\t\t\te: makeRes(\"10m\", \"1Mi\"),\n\t\t},\n\n\t\t\"multi-co\": {\n\t\t\tcc: []v1.Container{\n\t\t\t\tmakeContainer(\"c1\", false, \"10m\", \"1Mi\", \"20m\", \"2Mi\"),\n\t\t\t\tmakeContainer(\"c2\", false, \"10m\", \"1Mi\", \"40m\", \"4Mi\"),\n\t\t\t},\n\t\t\te: makeRes(\"20m\", \"2Mi\"),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tc, m, g := cosRequests(u.cc)\n\t\t\tassert.True(t, c.Equal(*u.e.Cpu()))\n\t\t\tassert.True(t, m.Equal(*u.e.Memory()))\n\t\t\tassert.True(t, g.Equal(*extractGPU(u.e)))\n\t\t})\n\t}\n}\n\nfunc Test_readinessGateStats(t *testing.T) {\n\tconst (\n\t\tgate1 = \"k9s.derailed.com/gate1\"\n\t\tgate2 = \"k9s.derailed.com/gate2\"\n\t)\n\n\tuu := map[string]struct {\n\t\tspec *v1.PodSpec\n\t\tst   *v1.PodStatus\n\t\tr    int\n\t\tt    int\n\t}{\n\t\t\"empty\": {\n\t\t\tspec: &v1.PodSpec{},\n\t\t\tst: &v1.PodStatus{\n\t\t\t\tConditions: []v1.PodCondition{{Type: v1.PodReady, Status: v1.ConditionTrue}},\n\t\t\t},\n\t\t\tr: 0,\n\t\t\tt: 0,\n\t\t},\n\t\t\"single\": {\n\t\t\tspec: &v1.PodSpec{\n\t\t\t\tReadinessGates: []v1.PodReadinessGate{{ConditionType: gate1}},\n\t\t\t},\n\t\t\tst: &v1.PodStatus{\n\t\t\t\tConditions: []v1.PodCondition{{Type: gate1, Status: v1.ConditionTrue}},\n\t\t\t},\n\t\t\tr: 1,\n\t\t\tt: 1,\n\t\t},\n\t\t\"multiple\": {\n\t\t\tspec: &v1.PodSpec{\n\t\t\t\tReadinessGates: []v1.PodReadinessGate{{ConditionType: gate1}, {ConditionType: gate2}},\n\t\t\t},\n\t\t\tst: &v1.PodStatus{\n\t\t\t\tConditions: []v1.PodCondition{{Type: gate1, Status: v1.ConditionTrue}, {Type: gate2, Status: v1.ConditionTrue}, {Type: v1.PodReady, Status: v1.ConditionFalse}},\n\t\t\t},\n\t\t\tr: 2,\n\t\t\tt: 2,\n\t\t},\n\t\t\"mixed\": {\n\t\t\tspec: &v1.PodSpec{\n\t\t\t\tReadinessGates: []v1.PodReadinessGate{{ConditionType: gate1}, {ConditionType: gate2}},\n\t\t\t},\n\t\t\tst: &v1.PodStatus{\n\t\t\t\tConditions: []v1.PodCondition{{Type: gate1, Status: v1.ConditionTrue}, {Type: gate2, Status: v1.ConditionFalse}, {Type: v1.PodReady, Status: v1.ConditionTrue}},\n\t\t\t},\n\t\t\tr: 1,\n\t\t\tt: 2,\n\t\t},\n\t\t\"missing\": {\n\t\t\tspec: &v1.PodSpec{\n\t\t\t\tReadinessGates: []v1.PodReadinessGate{{ConditionType: gate1}, {ConditionType: gate2}},\n\t\t\t},\n\t\t\tst: &v1.PodStatus{\n\t\t\t\tConditions: []v1.PodCondition{{Type: gate1, Status: v1.ConditionTrue}, {Type: v1.PodReady, Status: v1.ConditionTrue}},\n\t\t\t},\n\t\t\tr: 1,\n\t\t\tt: 2,\n\t\t},\n\t}\n\n\tvar p Pod\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tready, total := p.readinessGateStats(u.spec, u.st)\n\t\t\tassert.Equal(t, u.r, ready)\n\t\t\tassert.Equal(t, u.t, total)\n\t\t})\n\t}\n}\n\nfunc Test_diagnose(t *testing.T) {\n\tuu := map[string]struct {\n\t\tphase    string\n\t\tcr, ct   int\n\t\tready    bool\n\t\trgr, rgt int\n\t\terr      string\n\t}{\n\t\t\"completed\": {\n\t\t\tphase: Completed,\n\t\t\tcr:    0,\n\t\t\tct:    1,\n\t\t\tready: true,\n\t\t\trgr:   0,\n\t\t\trgt:   0,\n\t\t\terr:   \"\",\n\t\t},\n\t\t\"container-ready-check-failed\": {\n\t\t\tphase: \"Running\",\n\t\t\tcr:    1,\n\t\t\tct:    2,\n\t\t\tready: true,\n\t\t\trgr:   1,\n\t\t\trgt:   2,\n\t\t\terr:   \"container ready check failed: 1 of 2\",\n\t\t},\n\t\t\"readiness-gate-check-failed\": {\n\t\t\tphase: \"Running\",\n\t\t\tcr:    1,\n\t\t\tct:    1,\n\t\t\tready: true,\n\t\t\trgr:   1,\n\t\t\trgt:   2,\n\t\t\terr:   \"readiness gate check failed: 1 of 2\",\n\t\t},\n\t\t\"pod-condition-ready-false\": {\n\t\t\tphase: \"Running\",\n\t\t\tcr:    1,\n\t\t\tct:    1,\n\t\t\tready: false,\n\t\t\trgr:   0,\n\t\t\trgt:   0,\n\t\t\terr:   \"pod condition ready is false\",\n\t\t},\n\t\t\"pod-terminating\": {\n\t\t\tphase: \"Terminating\",\n\t\t\tcr:    1,\n\t\t\tct:    1,\n\t\t\tready: true,\n\t\t\trgr:   1,\n\t\t\trgt:   1,\n\t\t\terr:   \"pod is terminating\",\n\t\t},\n\t}\n\n\tvar p Pod\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\terr := p.diagnose(u.phase, u.cr, u.ct, u.ready, u.rgr, u.rgt)\n\t\t\tif u.err == \"\" {\n\t\t\t\tassert.NoError(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\tassert.Contains(t, err.Error(), u.err)\n\t\t\t}\n\t\t})\n\t}\n}\n\n// Helpers...\n\nfunc makeContainer(n string, restartable bool, rc, rm, lc, lm string) v1.Container {\n\talways := v1.ContainerRestartPolicyAlways\n\trq := v1.ResourceRequirements{\n\t\tRequests: makeRes(rc, rm),\n\t\tLimits:   makeRes(lc, lm),\n\t}\n\tvar rp *v1.ContainerRestartPolicy\n\tif restartable {\n\t\trp = &always\n\t}\n\n\treturn v1.Container{Name: n, Resources: rq, RestartPolicy: rp}\n}\n\nfunc makeRes(c, m string) v1.ResourceList {\n\tcpu, _ := res.ParseQuantity(c)\n\tmem, _ := res.ParseQuantity(m)\n\tgpu, _ := res.ParseQuantity(c)\n\n\treturn v1.ResourceList{\n\t\tv1.ResourceCPU:                    cpu,\n\t\tv1.ResourceMemory:                 mem,\n\t\tv1.ResourceName(\"nvidia.com/gpu\"): gpu,\n\t}\n}\n\nfunc makeCoMX(n, c, m string) mv1beta1.ContainerMetrics {\n\treturn mv1beta1.ContainerMetrics{\n\t\tName:  n,\n\t\tUsage: makeRes(c, m),\n\t}\n}\n\nfunc testTime() time.Time {\n\tt, err := time.Parse(time.RFC3339, \"2018-12-14T10:36:43.326972-07:00\")\n\tif err != nil {\n\t\tfmt.Println(\"TestTime Failed\", err)\n\t}\n\treturn t\n}\n"
  },
  {
    "path": "internal/render/pod_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\tv1 \"k8s.io/api/core/v1\"\n\tres \"k8s.io/apimachinery/pkg/api/resource\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\tmv1beta1 \"k8s.io/metrics/pkg/apis/metrics/v1beta1\"\n)\n\nfunc init() {\n\tmodel1.AddColor = tcell.ColorBlue\n\tmodel1.HighlightColor = tcell.ColorYellow\n\tmodel1.CompletedColor = tcell.ColorGray\n\tmodel1.StdColor = tcell.ColorWhite\n\tmodel1.ErrColor = tcell.ColorRed\n\tmodel1.KillColor = tcell.ColorGray\n}\n\nfunc TestPodColorer(t *testing.T) {\n\tstdHeader := model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\t\tmodel1.HeaderColumn{Name: \"NAME\"},\n\t\tmodel1.HeaderColumn{Name: \"READY\"},\n\t\tmodel1.HeaderColumn{Name: \"RESTARTS\"},\n\t\tmodel1.HeaderColumn{Name: \"STATUS\"},\n\t\tmodel1.HeaderColumn{Name: \"VALID\"},\n\t}\n\n\tuu := map[string]struct {\n\t\tre model1.RowEvent\n\t\th  model1.Header\n\t\te  tcell.Color\n\t}{\n\t\t\"valid\": {\n\t\t\th: stdHeader,\n\t\t\tre: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"blee\", \"fred\", \"1/1\", \"0\", render.Running, \"\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: model1.StdColor,\n\t\t},\n\t\t\"init\": {\n\t\t\th: stdHeader,\n\t\t\tre: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"blee\", \"fred\", \"1/1\", \"0\", render.PodInitializing, \"\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: model1.AddColor,\n\t\t},\n\t\t\"init-err\": {\n\t\t\th: stdHeader,\n\t\t\tre: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"blee\", \"fred\", \"1/1\", \"0\", render.PodInitializing, \"blah\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: model1.AddColor,\n\t\t},\n\t\t\"initialized\": {\n\t\t\th: stdHeader,\n\t\t\tre: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"blee\", \"fred\", \"1/1\", \"0\", render.Initialized, \"blah\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: model1.HighlightColor,\n\t\t},\n\t\t\"completed\": {\n\t\t\th: stdHeader,\n\t\t\tre: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"blee\", \"fred\", \"1/1\", \"0\", render.Completed, \"blah\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: model1.CompletedColor,\n\t\t},\n\t\t\"terminating\": {\n\t\t\th: stdHeader,\n\t\t\tre: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"blee\", \"fred\", \"1/1\", \"0\", render.Terminating, \"blah\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: model1.KillColor,\n\t\t},\n\t\t\"invalid\": {\n\t\t\th: stdHeader,\n\t\t\tre: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"blee\", \"fred\", \"1/1\", \"0\", \"Running\", \"blah\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: model1.ErrColor,\n\t\t},\n\t\t\"unknown-cool\": {\n\t\t\th: stdHeader,\n\t\t\tre: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"blee\", \"fred\", \"1/1\", \"0\", \"blee\", \"\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: model1.AddColor,\n\t\t},\n\t\t\"unknown-err\": {\n\t\t\th: stdHeader,\n\t\t\tre: model1.RowEvent{\n\t\t\t\tKind: model1.EventAdd,\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"blee\", \"fred\", \"1/1\", \"0\", \"blee\", \"doh\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: model1.ErrColor,\n\t\t},\n\t\t\"status\": {\n\t\t\th: stdHeader[0:3],\n\t\t\tre: model1.RowEvent{\n\t\t\t\tKind: model1.EventDelete,\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"blee\", \"fred\", \"1/1\", \"0\", \"blee\", \"\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: model1.KillColor,\n\t\t},\n\t}\n\n\tvar r render.Pod\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, r.ColorerFunc()(\"\", u.h, &u.re))\n\t\t})\n\t}\n}\n\nfunc TestPodRender(t *testing.T) {\n\tpom := render.PodWithMetrics{\n\t\tRaw: load(t, \"po\"),\n\t\tMX:  makePodMX(\"nginx\", \"100m\", \"50Mi\"),\n\t}\n\n\tpo := render.NewPod()\n\tr := model1.NewRow(14)\n\terr := po.Render(&pom, \"\", &r)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"default/nginx\", r.ID)\n\te := model1.Fields{\"default\", \"nginx\", \"n/a\", \"●\", \"1/1\", \"Running\", \"0\", \"<unknown>\", \"100\", \"100:0\", \"100\", \"n/a\", \"50\", \"70:170\", \"71\", \"29\", \"0:0\", \"172.17.0.6\", \"minikube\", \"default\", \"<none>\"}\n\tassert.Equal(t, e, r.Fields[:21])\n}\n\nfunc BenchmarkPodRender(b *testing.B) {\n\tpom := render.PodWithMetrics{\n\t\tRaw: load(b, \"po\"),\n\t\tMX:  makePodMX(\"nginx\", \"10m\", \"10Mi\"),\n\t}\n\tpo := render.NewPod()\n\tr := model1.NewRow(12)\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor range b.N {\n\t\t_ = po.Render(&pom, \"\", &r)\n\t}\n}\n\nfunc TestPodInitRender(t *testing.T) {\n\tpom := render.PodWithMetrics{\n\t\tRaw: load(t, \"po_init\"),\n\t\tMX:  makePodMX(\"nginx\", \"10m\", \"10Mi\"),\n\t}\n\n\tpo := render.NewPod()\n\tr := model1.NewRow(14)\n\terr := po.Render(&pom, \"\", &r)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"default/nginx\", r.ID)\n\te := model1.Fields{\"default\", \"nginx\", \"n/a\", \"●\", \"1/1\", \"Init:0/1\", \"0\", \"<unknown>\", \"10\", \"100:0\", \"10\", \"n/a\", \"10\", \"70:170\", \"14\", \"5\", \"0:0\", \"172.17.0.6\", \"minikube\", \"default\", \"<none>\"}\n\tassert.Equal(t, e, r.Fields[:21])\n}\n\nfunc TestPodSidecarRender(t *testing.T) {\n\tpom := render.PodWithMetrics{\n\t\tRaw: load(t, \"po_sidecar\"),\n\t\tMX:  makePodMX(\"sleep\", \"100m\", \"40Mi\"),\n\t}\n\n\tpo := render.NewPod()\n\tr := model1.NewRow(14)\n\terr := po.Render(&pom, \"\", &r)\n\trequire.NoError(t, err)\n\n\tassert.Equal(t, \"default/sleep\", r.ID)\n\te := model1.Fields{\"default\", \"sleep\", \"n/a\", \"●\", \"2/2\", \"Running\", \"0\", \"<unknown>\", \"100\", \"50:250\", \"200\", \"40\", \"40\", \"50:80\", \"80\", \"50\", \"0:0\", \"10.244.0.8\", \"kind-control-plane\", \"default\", \"<none>\"}\n\tassert.Equal(t, e, r.Fields[:21])\n}\n\nfunc TestCheckPodStatus(t *testing.T) {\n\tuu := map[string]struct {\n\t\tpod v1.Pod\n\t\te   string\n\t}{\n\t\t\"unknown\": {\n\t\t\tpod: v1.Pod{\n\t\t\t\tStatus: v1.PodStatus{\n\t\t\t\t\tPhase: render.PhaseUnknown,\n\t\t\t\t},\n\t\t\t},\n\t\t\te: render.PhaseUnknown,\n\t\t},\n\t\t\"running\": {\n\t\t\tpod: v1.Pod{\n\t\t\t\tStatus: v1.PodStatus{\n\t\t\t\t\tPhase:                 v1.PodRunning,\n\t\t\t\t\tInitContainerStatuses: []v1.ContainerStatus{},\n\t\t\t\t\tContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"c1\",\n\t\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\t\tRunning: &v1.ContainerStateRunning{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: render.PhaseRunning,\n\t\t},\n\t\t\"gated\": {\n\t\t\tpod: v1.Pod{\n\t\t\t\tStatus: v1.PodStatus{\n\t\t\t\t\tConditions: []v1.PodCondition{\n\t\t\t\t\t\t{Type: v1.PodScheduled, Reason: v1.PodReasonSchedulingGated},\n\t\t\t\t\t},\n\t\t\t\t\tPhase:                 v1.PodRunning,\n\t\t\t\t\tInitContainerStatuses: []v1.ContainerStatus{},\n\t\t\t\t\tContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"c1\",\n\t\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\t\tRunning: &v1.ContainerStateRunning{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: v1.PodReasonSchedulingGated,\n\t\t},\n\n\t\t\"backoff\": {\n\t\t\tpod: v1.Pod{\n\t\t\t\tStatus: v1.PodStatus{\n\t\t\t\t\tPhase: v1.PodRunning,\n\t\t\t\t\tContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"c1\",\n\t\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\t\tWaiting: &v1.ContainerStateWaiting{\n\t\t\t\t\t\t\t\t\tReason: render.PhaseImagePullBackOff,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: render.PhaseImagePullBackOff,\n\t\t},\n\t\t\"backoff-init\": {\n\t\t\tpod: v1.Pod{\n\t\t\t\tStatus: v1.PodStatus{\n\t\t\t\t\tPhase: v1.PodRunning,\n\t\t\t\t\tInitContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"ic1\",\n\t\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\t\tWaiting: &v1.ContainerStateWaiting{\n\t\t\t\t\t\t\t\t\tReason: render.PhaseImagePullBackOff,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"c1\",\n\t\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\t\tWaiting: &v1.ContainerStateWaiting{\n\t\t\t\t\t\t\t\t\tReason: render.PhaseImagePullBackOff,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"Init:ImagePullBackOff\",\n\t\t},\n\n\t\t\"init-terminated-cool\": {\n\t\t\tpod: v1.Pod{\n\t\t\t\tStatus: v1.PodStatus{\n\t\t\t\t\tPhase: v1.PodRunning,\n\t\t\t\t\tInitContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"ic1\",\n\t\t\t\t\t\t\tState: v1.ContainerState{},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"c1\",\n\t\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\t\tWaiting: &v1.ContainerStateWaiting{\n\t\t\t\t\t\t\t\t\tReason: render.PhaseImagePullBackOff,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"Init:0/0\",\n\t\t},\n\n\t\t\"init-terminated-reason\": {\n\t\t\tpod: v1.Pod{\n\t\t\t\tStatus: v1.PodStatus{\n\t\t\t\t\tPhase: v1.PodRunning,\n\t\t\t\t\tInitContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"ic1\",\n\t\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\t\tTerminated: &v1.ContainerStateTerminated{\n\t\t\t\t\t\t\t\t\tExitCode: 1,\n\t\t\t\t\t\t\t\t\tReason:   \"blah\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"c1\",\n\t\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\t\tWaiting: &v1.ContainerStateWaiting{\n\t\t\t\t\t\t\t\t\tReason: render.PhaseImagePullBackOff,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"Init:blah\",\n\t\t},\n\t\t\"init-terminated-sig\": {\n\t\t\tpod: v1.Pod{\n\t\t\t\tStatus: v1.PodStatus{\n\t\t\t\t\tPhase: v1.PodRunning,\n\t\t\t\t\tInitContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"ic1\",\n\t\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\t\tTerminated: &v1.ContainerStateTerminated{\n\t\t\t\t\t\t\t\t\tExitCode: 2,\n\t\t\t\t\t\t\t\t\tSignal:   9,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"c1\",\n\t\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\t\tWaiting: &v1.ContainerStateWaiting{\n\t\t\t\t\t\t\t\t\tReason: render.PhaseImagePullBackOff,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"Init:Signal:9\",\n\t\t},\n\t\t\"init-terminated-code\": {\n\t\t\tpod: v1.Pod{\n\t\t\t\tStatus: v1.PodStatus{\n\t\t\t\t\tPhase: v1.PodRunning,\n\t\t\t\t\tInitContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"ic1\",\n\t\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\t\tTerminated: &v1.ContainerStateTerminated{\n\t\t\t\t\t\t\t\t\tExitCode: 2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"c1\",\n\t\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\t\tWaiting: &v1.ContainerStateWaiting{\n\t\t\t\t\t\t\t\t\tReason: render.PhaseImagePullBackOff,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"Init:ExitCode:2\",\n\t\t},\n\n\t\t\"co-reason\": {\n\t\t\tpod: v1.Pod{\n\t\t\t\tStatus: v1.PodStatus{\n\t\t\t\t\tPhase: v1.PodRunning,\n\t\t\t\t\tContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"c1\",\n\t\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\t\tTerminated: &v1.ContainerStateTerminated{\n\t\t\t\t\t\t\t\t\tReason: \"blah\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"blah\",\n\t\t},\n\t\t\"co-reason-ready\": {\n\t\t\tpod: v1.Pod{\n\t\t\t\tStatus: v1.PodStatus{\n\t\t\t\t\tPhase: v1.PodRunning,\n\t\t\t\t\tContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"c1\",\n\t\t\t\t\t\t\tReady: true,\n\t\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\t\tRunning: &v1.ContainerStateRunning{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"Running\",\n\t\t},\n\t\t\"co-reason-completed\": {\n\t\t\tpod: v1.Pod{\n\t\t\t\tStatus: v1.PodStatus{\n\t\t\t\t\tConditions: []v1.PodCondition{\n\t\t\t\t\t\t{Type: v1.PodReady, Status: v1.ConditionTrue},\n\t\t\t\t\t},\n\t\t\t\t\tPhase: render.PhaseCompleted,\n\t\t\t\t\tContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:  \"c1\",\n\t\t\t\t\t\t\tReady: true,\n\t\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\t\tRunning: &v1.ContainerStateRunning{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"Running\",\n\t\t},\n\n\t\t\"co-sig\": {\n\t\t\tpod: v1.Pod{\n\t\t\t\tStatus: v1.PodStatus{\n\t\t\t\t\tPhase: v1.PodRunning,\n\t\t\t\t\tContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"c1\",\n\t\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\t\tTerminated: &v1.ContainerStateTerminated{\n\t\t\t\t\t\t\t\t\tExitCode: 2,\n\t\t\t\t\t\t\t\t\tSignal:   9,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"Signal:9\",\n\t\t},\n\t\t\"co-code\": {\n\t\t\tpod: v1.Pod{\n\t\t\t\tStatus: v1.PodStatus{\n\t\t\t\t\tPhase: v1.PodRunning,\n\t\t\t\t\tContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"c1\",\n\t\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\t\tTerminated: &v1.ContainerStateTerminated{\n\t\t\t\t\t\t\t\t\tExitCode: 2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"ExitCode:2\",\n\t\t},\n\t\t\"co-ready\": {\n\t\t\tpod: v1.Pod{\n\t\t\t\tStatus: v1.PodStatus{\n\t\t\t\t\tPhase: v1.PodRunning,\n\t\t\t\t\tContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"c1\",\n\t\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\t\tRunning: &v1.ContainerStateRunning{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"Running\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, render.PodStatus(&u.pod))\n\t\t})\n\t}\n}\n\nfunc TestCheckPhase(t *testing.T) {\n\talways := v1.ContainerRestartPolicyAlways\n\tuu := map[string]struct {\n\t\tpod v1.Pod\n\t\te   string\n\t}{\n\t\t\"unknown\": {\n\t\t\tpod: v1.Pod{\n\t\t\t\tStatus: v1.PodStatus{\n\t\t\t\t\tPhase: render.PhaseUnknown,\n\t\t\t\t},\n\t\t\t},\n\t\t\te: render.PhaseUnknown,\n\t\t},\n\t\t\"terminating\": {\n\t\t\tpod: v1.Pod{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tDeletionTimestamp: &metav1.Time{Time: testTime()},\n\t\t\t\t},\n\t\t\t\tStatus: v1.PodStatus{\n\t\t\t\t\tPhase:  render.PhaseUnknown,\n\t\t\t\t\tReason: \"bla\",\n\t\t\t\t},\n\t\t\t},\n\t\t\te: render.PhaseTerminating,\n\t\t},\n\t\t\"terminating-toast-node\": {\n\t\t\tpod: v1.Pod{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tDeletionTimestamp: &metav1.Time{Time: testTime()},\n\t\t\t\t},\n\t\t\t\tStatus: v1.PodStatus{\n\t\t\t\t\tPhase:  render.PhaseUnknown,\n\t\t\t\t\tReason: render.NodeUnreachablePodReason,\n\t\t\t\t},\n\t\t\t},\n\t\t\te: render.PhaseUnknown,\n\t\t},\n\t\t\"restartable\": {\n\t\t\tpod: v1.Pod{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tDeletionTimestamp: &metav1.Time{Time: testTime()},\n\t\t\t\t},\n\t\t\t\tSpec: v1.PodSpec{\n\t\t\t\t\tInitContainers: []v1.Container{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:          \"ic1\",\n\t\t\t\t\t\t\tRestartPolicy: &always,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tStatus: v1.PodStatus{\n\t\t\t\t\tPhase:  render.PhaseUnknown,\n\t\t\t\t\tReason: \"bla\",\n\t\t\t\t\tInitContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"ic1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"Init:0/1\",\n\t\t},\n\t\t\"waiting\": {\n\t\t\tpod: v1.Pod{\n\t\t\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\t\t\tDeletionTimestamp: &metav1.Time{Time: testTime()},\n\t\t\t\t},\n\t\t\t\tSpec: v1.PodSpec{\n\t\t\t\t\tInitContainers: []v1.Container{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:          \"ic1\",\n\t\t\t\t\t\t\tRestartPolicy: &always,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tContainers: []v1.Container{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"c1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tStatus: v1.PodStatus{\n\t\t\t\t\tPhase:  render.PhaseUnknown,\n\t\t\t\t\tReason: \"bla\",\n\t\t\t\t\tInitContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"ic1\",\n\t\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\t\tRunning: &v1.ContainerStateRunning{},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tContainerStatuses: []v1.ContainerStatus{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName: \"c1\",\n\t\t\t\t\t\t\tState: v1.ContainerState{\n\t\t\t\t\t\t\t\tWaiting: &v1.ContainerStateWaiting{\n\t\t\t\t\t\t\t\t\tReason: \"bla\",\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\te: \"Init:0/1\",\n\t\t},\n\t}\n\n\tvar p render.Pod\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, p.Phase(u.pod.DeletionTimestamp, &u.pod.Spec, &u.pod.Status))\n\t\t})\n\t}\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc makePodMX(name, cpu, mem string) *mv1beta1.PodMetrics {\n\treturn &mv1beta1.PodMetrics{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:      name,\n\t\t\tNamespace: \"default\",\n\t\t},\n\t\tContainers: []mv1beta1.ContainerMetrics{\n\t\t\t{Usage: makeRes(cpu, mem)},\n\t\t},\n\t}\n}\n\nfunc makeRes(c, m string) v1.ResourceList {\n\tcpu, _ := res.ParseQuantity(c)\n\tmem, _ := res.ParseQuantity(m)\n\n\treturn v1.ResourceList{\n\t\tv1.ResourceCPU:    cpu,\n\t\tv1.ResourceMemory: mem,\n\t}\n}\n"
  },
  {
    "path": "internal/render/policy.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n)\n\nfunc rbacVerbHeader() model1.Header {\n\treturn model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"GET   \"},\n\t\tmodel1.HeaderColumn{Name: \"LIST  \"},\n\t\tmodel1.HeaderColumn{Name: \"WATCH \"},\n\t\tmodel1.HeaderColumn{Name: \"CREATE\"},\n\t\tmodel1.HeaderColumn{Name: \"PATCH \"},\n\t\tmodel1.HeaderColumn{Name: \"UPDATE\"},\n\t\tmodel1.HeaderColumn{Name: \"DELETE\"},\n\t\tmodel1.HeaderColumn{Name: \"DEL-LIST \"},\n\t\tmodel1.HeaderColumn{Name: \"EXTRAS\", Attrs: model1.Attrs{Wide: true}},\n\t}\n}\n\n// Policy renders a rbac policy to screen.\ntype Policy struct {\n\tBase\n}\n\n// ColorerFunc colors a resource row.\nfunc (Policy) ColorerFunc() model1.ColorerFunc {\n\treturn func(string, model1.Header, *model1.RowEvent) tcell.Color {\n\t\treturn tcell.ColorMediumSpringGreen\n\t}\n}\n\n// Header returns a header row.\nfunc (Policy) Header(string) model1.Header {\n\th := model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\t\tmodel1.HeaderColumn{Name: \"NAME\"},\n\t\tmodel1.HeaderColumn{Name: \"API-GROUP\"},\n\t\tmodel1.HeaderColumn{Name: \"BINDING\"},\n\t}\n\th = append(h, rbacVerbHeader()...)\n\th = append(h, model1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}})\n\n\treturn h\n}\n\n// Render renders a K8s resource to screen.\nfunc (Policy) Render(o any, _ string, r *model1.Row) error {\n\tp, ok := o.(*PolicyRes)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting PolicyRes but got %T\", o)\n\t}\n\n\tr.ID = client.FQN(p.Namespace, p.Resource)\n\tr.Fields = append(r.Fields,\n\t\tp.Namespace,\n\t\tcleanseResource(p.Resource),\n\t\tp.Group,\n\t\tp.Binding,\n\t)\n\tr.Fields = append(r.Fields, asVerbs(p.Verbs)...)\n\tr.Fields = append(r.Fields, \"\")\n\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc cleanseResource(r string) string {\n\tif r == \"\" || r[0] == '/' {\n\t\treturn r\n\t}\n\ttt := strings.Split(r, \"/\")\n\tswitch len(tt) {\n\tcase 2, 3:\n\t\treturn strings.TrimPrefix(r, tt[0]+\"/\")\n\tdefault:\n\t\treturn r\n\t}\n}\n\n// PolicyRes represents a rbac policy rule.\ntype PolicyRes struct {\n\tNamespace, Binding string\n\tResource, Group    string\n\tResourceName       string\n\tNonResourceURL     string\n\tVerbs              []string\n}\n\n// NewPolicyRes returns a new policy.\nfunc NewPolicyRes(ns, binding, res, grp string, vv []string) *PolicyRes {\n\treturn &PolicyRes{\n\t\tNamespace: ns,\n\t\tBinding:   binding,\n\t\tResource:  res,\n\t\tGroup:     grp,\n\t\tVerbs:     vv,\n\t}\n}\n\n// GR returns the group/resource path.\nfunc (p *PolicyRes) GR() string {\n\treturn p.Group + \"/\" + p.Resource\n}\n\n// Merge merges two policies.\nfunc (p *PolicyRes) Merge(p1 *PolicyRes) (*PolicyRes, error) {\n\tif p.GR() != p1.GR() {\n\t\treturn nil, fmt.Errorf(\"policy mismatch %s vs %s\", p.GR(), p1.GR())\n\t}\n\n\tfor _, v := range p1.Verbs {\n\t\tif !p.hasVerb(v) {\n\t\t\tp.Verbs = append(p.Verbs, v)\n\t\t}\n\t}\n\n\treturn p, nil\n}\n\nfunc (p *PolicyRes) hasVerb(v1 string) bool {\n\tfor _, v := range p.Verbs {\n\t\tif v == v1 {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// GetObjectKind returns a schema object.\nfunc (*PolicyRes) GetObjectKind() schema.ObjectKind {\n\treturn nil\n}\n\n// DeepCopyObject returns a container copy.\nfunc (p *PolicyRes) DeepCopyObject() runtime.Object {\n\treturn p\n}\n\n// Policies represents a collection of RBAC policies.\ntype Policies []*PolicyRes\n\n// Upsert adds a new policy.\nfunc (pp Policies) Upsert(p *PolicyRes) Policies {\n\tidx, ok := pp.find(p.GR())\n\tif !ok {\n\t\treturn append(pp, p)\n\t}\n\tp, err := pp[idx].Merge(p)\n\tif err != nil {\n\t\tslog.Error(\"Policy upsert failed\", slogs.Error, err)\n\t\treturn pp\n\t}\n\tpp[idx] = p\n\n\treturn pp\n}\n\n// Find locates a row by id. Returns false is not found.\nfunc (pp Policies) find(gr string) (int, bool) {\n\tfor i, p := range pp {\n\t\tif p.GR() == gr {\n\t\t\treturn i, true\n\t\t}\n\t}\n\n\treturn 0, false\n}\n"
  },
  {
    "path": "internal/render/policy_int_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_cleanseResource(t *testing.T) {\n\tuu := map[string]struct {\n\t\tr, e string\n\t}{\n\t\t\"empty\": {},\n\t\t\"single\": {\n\t\t\tr: \"fred\",\n\t\t\te: \"fred\",\n\t\t},\n\t\t\"grp/res\": {\n\t\t\tr: \"fred/blee\",\n\t\t\te: \"blee\",\n\t\t},\n\t\t\"grp/res/sub\": {\n\t\t\tr: \"fred/blee/bob\",\n\t\t\te: \"blee/bob\",\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, cleanseResource(u.r))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/render/policy_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPolicyResMerge(t *testing.T) {\n\tuu := map[string]struct {\n\t\tp1, p2, e *render.PolicyRes\n\t\terr       error\n\t}{\n\t\t\"simple\": {\n\t\t\tp1: render.NewPolicyRes(\"fred\", \"blee\", \"deployments\", \"apps/v1\", []string{\"get\"}),\n\t\t\tp2: render.NewPolicyRes(\"fred\", \"blee\", \"deployments\", \"apps/v1\", []string{\"patch\"}),\n\t\t\te:  render.NewPolicyRes(\"fred\", \"blee\", \"deployments\", \"apps/v1\", []string{\"get\", \"patch\"}),\n\t\t},\n\t\t\"dups\": {\n\t\t\tp1: render.NewPolicyRes(\"fred\", \"blee\", \"deployments\", \"apps/v1\", []string{\"get\"}),\n\t\t\tp2: render.NewPolicyRes(\"fred\", \"blee\", \"deployments\", \"apps/v1\", []string{\"get\", \"delete\"}),\n\t\t\te:  render.NewPolicyRes(\"fred\", \"blee\", \"deployments\", \"apps/v1\", []string{\"get\", \"delete\"}),\n\t\t},\n\t\t\"mismatch\": {\n\t\t\tp1:  render.NewPolicyRes(\"fred\", \"blee\", \"deployments\", \"apps/v1\", []string{\"get\"}),\n\t\t\tp2:  render.NewPolicyRes(\"fred\", \"blee\", \"statefulsets\", \"apps/v1\", []string{\"get\", \"delete\"}),\n\t\t\terr: errors.New(\"policy mismatch apps/v1/deployments vs apps/v1/statefulsets\"),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\te, err := u.p1.Merge(u.p2)\n\t\t\tassert.Equal(t, u.err, err)\n\t\t\tassert.Equal(t, u.e, e)\n\t\t})\n\t}\n}\n\nfunc TestPolicyRender(t *testing.T) {\n\tvar p render.Policy\n\n\tvar r model1.Row\n\to := render.PolicyRes{\n\t\tNamespace:      \"blee\",\n\t\tBinding:        \"fred\",\n\t\tResource:       \"res\",\n\t\tGroup:          \"grp\",\n\t\tResourceName:   \"bob\",\n\t\tNonResourceURL: \"/blee\",\n\t\tVerbs:          []string{\"get\", \"list\", \"watch\"},\n\t}\n\n\trequire.NoError(t, p.Render(&o, \"fred\", &r))\n\tassert.Equal(t, \"blee/res\", r.ID)\n\tassert.Equal(t, model1.Fields{\n\t\t\"blee\",\n\t\t\"res\",\n\t\t\"grp\",\n\t\t\"fred\",\n\t\t\"[green::b] ✓ [::]\",\n\t\t\"[green::b] ✓ [::]\",\n\t\t\"[green::b] ✓ [::]\",\n\t\t\"[orangered::b] × [::]\",\n\t\t\"[orangered::b] × [::]\",\n\t\t\"[orangered::b] × [::]\",\n\t\t\"[orangered::b] × [::]\",\n\t\t\"[orangered::b] × [::]\",\n\t\t\"\",\n\t\t\"\",\n\t}, r.Fields)\n}\n"
  },
  {
    "path": "internal/render/port_forward_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPortForwardRender(t *testing.T) {\n\to := render.ForwardRes{\n\t\tForwarder: fwd{},\n\t\tConfig: render.BenchCfg{\n\t\t\tC:    1,\n\t\t\tN:    1,\n\t\t\tHost: \"0.0.0.0\",\n\t\t\tPath: \"/\",\n\t\t},\n\t}\n\n\tvar p render.PortForward\n\tvar r model1.Row\n\trequire.NoError(t, p.Render(o, \"fred\", &r))\n\tassert.Equal(t, \"blee/fred\", r.ID)\n\tassert.Equal(t, model1.Fields{\n\t\t\"blee\",\n\t\t\"fred\",\n\t\t\"co\",\n\t\t\"p1:p2\",\n\t\t\"http://0.0.0.0:p1/\",\n\t\t\"1\",\n\t\t\"1\",\n\t\t\"\",\n\t}, r.Fields[:8])\n}\n\n// Helpers...\n\ntype fwd struct{}\n\nfunc (fwd) ID() string {\n\treturn \"blee/fred\"\n}\n\nfunc (fwd) Path() string {\n\treturn \"blee/fred\"\n}\n\nfunc (fwd) Container() string {\n\treturn \"co\"\n}\n\nfunc (fwd) Port() string {\n\treturn \"p1:p2\"\n}\n\nfunc (fwd) Active() bool {\n\treturn true\n}\n\nfunc (fwd) Age() time.Time {\n\treturn testTime()\n}\n\nfunc (fwd) Address() string {\n\treturn \"\"\n}\n"
  },
  {
    "path": "internal/render/portforward.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/tcell/v2\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n)\n\n// Forwarder represents a port forwarder.\ntype Forwarder interface {\n\t// ID returns the PF FQN.\n\tID() string\n\n\t// Container returns a container name.\n\tContainer() string\n\n\t// Port returns container exposed port.\n\tPort() string\n\n\t// Address returns the host address.\n\tAddress() string\n\n\t// Active returns forwarder current state.\n\tActive() bool\n\n\t// Age returns forwarder age.\n\tAge() time.Time\n}\n\n// PortForward renders a portforwards to screen.\ntype PortForward struct {\n\tBase\n}\n\n// ColorerFunc colors a resource row.\nfunc (PortForward) ColorerFunc() model1.ColorerFunc {\n\treturn func(string, model1.Header, *model1.RowEvent) tcell.Color {\n\t\treturn tcell.ColorSkyblue\n\t}\n}\n\n// Header returns a header row.\nfunc (PortForward) Header(string) model1.Header {\n\treturn model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\t\tmodel1.HeaderColumn{Name: \"NAME\"},\n\t\tmodel1.HeaderColumn{Name: \"CONTAINER\"},\n\t\tmodel1.HeaderColumn{Name: \"PORTS\"},\n\t\tmodel1.HeaderColumn{Name: \"URL\"},\n\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t\tmodel1.HeaderColumn{Name: \"N\"},\n\t\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\t\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n\t}\n}\n\n// Render renders a K8s resource to screen.\nfunc (PortForward) Render(o any, _ string, r *model1.Row) error {\n\tpf, ok := o.(ForwardRes)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting a ForwardRes but got %T\", o)\n\t}\n\n\tports := strings.Split(pf.Port(), \":\")\n\tr.ID = pf.ID()\n\tns, n := client.Namespaced(r.ID)\n\n\tr.Fields = model1.Fields{\n\t\tns,\n\t\ttrimContainer(n),\n\t\tpf.Container(),\n\t\tpf.Port(),\n\t\tUrlFor(pf.Config.Host, pf.Config.Path, ports[0], pf.Address()),\n\t\tAsThousands(int64(pf.Config.C)),\n\t\tAsThousands(int64(pf.Config.N)),\n\t\t\"\",\n\t\tToAge(metav1.Time{Time: pf.Age()}),\n\t}\n\n\treturn nil\n}\n\n// Helpers...\n\nfunc trimContainer(n string) string {\n\ttokens := strings.Split(n, \"|\")\n\tif len(tokens) == 0 {\n\t\treturn n\n\t}\n\t_, name := client.Namespaced(tokens[0])\n\n\treturn name\n}\n\n// UrlFor computes fq url for a given benchmark configuration.\nfunc UrlFor(host, path, port, address string) string {\n\tif host == \"\" {\n\t\thost = address\n\t}\n\tif path == \"\" {\n\t\tpath = \"/\"\n\t}\n\n\treturn \"http://\" + host + \":\" + port + path\n}\n\n// BenchCfg represents a benchmark configuration.\ntype BenchCfg struct {\n\tC, N       int\n\tHost, Path string\n}\n\n// ForwardRes represents a benchmark resource.\ntype ForwardRes struct {\n\tForwarder\n\tConfig BenchCfg\n}\n\n// GetObjectKind returns a schema object.\nfunc (ForwardRes) GetObjectKind() schema.ObjectKind {\n\treturn nil\n}\n\n// DeepCopyObject returns a container copy.\nfunc (f ForwardRes) DeepCopyObject() runtime.Object {\n\treturn f\n}\n"
  },
  {
    "path": "internal/render/pv.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/tcell/v2\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nconst terminatingPhase = \"Terminating\"\n\n// PersistentVolume renders a K8s PersistentVolume to screen.\ntype PersistentVolume struct {\n\tBase\n}\n\n// ColorerFunc colors a resource row.\nfunc (PersistentVolume) ColorerFunc() model1.ColorerFunc {\n\treturn func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color {\n\t\tc := model1.DefaultColorer(ns, h, re)\n\n\t\tidx, ok := h.IndexOf(\"STATUS\", true)\n\t\tif !ok {\n\t\t\treturn c\n\t\t}\n\t\tswitch strings.TrimSpace(re.Row.Fields[idx]) {\n\t\tcase string(v1.VolumeBound):\n\t\t\treturn model1.StdColor\n\t\tcase string(v1.VolumeAvailable):\n\t\t\treturn tcell.ColorGreen\n\t\tcase string(v1.VolumePending):\n\t\t\treturn model1.PendingColor\n\t\tcase terminatingPhase:\n\t\t\treturn model1.CompletedColor\n\t\t}\n\n\t\treturn c\n\t}\n}\n\n// Header returns a header row.\nfunc (p PersistentVolume) Header(_ string) model1.Header {\n\treturn p.doHeader(defaultPVHeader)\n}\n\nvar defaultPVHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"CAPACITY\", Attrs: model1.Attrs{Capacity: true}},\n\tmodel1.HeaderColumn{Name: \"ACCESS MODES\"},\n\tmodel1.HeaderColumn{Name: \"RECLAIM POLICY\"},\n\tmodel1.HeaderColumn{Name: \"STATUS\"},\n\tmodel1.HeaderColumn{Name: \"CLAIM\"},\n\tmodel1.HeaderColumn{Name: \"STORAGECLASS\"},\n\tmodel1.HeaderColumn{Name: \"REASON\"},\n\tmodel1.HeaderColumn{Name: \"VOLUMEMODE\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"LABELS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// Render renders a K8s resource to screen.\nfunc (p PersistentVolume) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tif err := p.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif p.specs.isEmpty() {\n\t\treturn nil\n\t}\n\tcols, err := p.specs.realize(raw, defaultPVHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\nfunc (p PersistentVolume) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {\n\tvar pv v1.PersistentVolume\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &pv)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tphase := pv.Status.Phase\n\tif pv.DeletionTimestamp != nil {\n\t\tphase = terminatingPhase\n\t}\n\tvar claim string\n\tif pv.Spec.ClaimRef != nil {\n\t\tclaim = path.Join(pv.Spec.ClaimRef.Namespace, pv.Spec.ClaimRef.Name)\n\t}\n\tclass, found := pv.Annotations[v1.BetaStorageClassAnnotation]\n\tif !found {\n\t\tclass = pv.Spec.StorageClassName\n\t}\n\n\tsize := pv.Spec.Capacity[v1.ResourceStorage]\n\n\tr.ID = client.MetaFQN(&pv.ObjectMeta)\n\tr.Fields = model1.Fields{\n\t\tpv.Name,\n\t\tsize.String(),\n\t\taccessMode(pv.Spec.AccessModes),\n\t\tstring(pv.Spec.PersistentVolumeReclaimPolicy),\n\t\tstring(phase),\n\t\tclaim,\n\t\tclass,\n\t\tpv.Status.Reason,\n\t\tp.volumeMode(pv.Spec.VolumeMode),\n\t\tmapToStr(pv.Labels),\n\t\tAsStatus(p.diagnose(phase)),\n\t\tToAge(pv.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n\nfunc (PersistentVolume) diagnose(phase v1.PersistentVolumePhase) error {\n\tif phase == v1.VolumeFailed {\n\t\treturn fmt.Errorf(\"failed to delete or recycle\")\n\t}\n\n\treturn nil\n}\n\nfunc (PersistentVolume) volumeMode(m *v1.PersistentVolumeMode) string {\n\tif m == nil {\n\t\treturn MissingValue\n\t}\n\n\treturn string(*m)\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc accessMode(aa []v1.PersistentVolumeAccessMode) string {\n\tdd := accessDedup(aa)\n\ts := make([]string, 0, len(dd))\n\tfor _, am := range dd {\n\t\tswitch am {\n\t\tcase v1.ReadWriteOnce:\n\t\t\ts = append(s, \"RWO\")\n\t\tcase v1.ReadOnlyMany:\n\t\t\ts = append(s, \"ROX\")\n\t\tcase v1.ReadWriteMany:\n\t\t\ts = append(s, \"RWX\")\n\t\tcase v1.ReadWriteOncePod:\n\t\t\ts = append(s, \"RWOP\")\n\t\t}\n\t}\n\n\treturn strings.Join(s, \",\")\n}\n\nfunc accessContains(cc []v1.PersistentVolumeAccessMode, a v1.PersistentVolumeAccessMode) bool {\n\tfor _, c := range cc {\n\t\tif c == a {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc accessDedup(cc []v1.PersistentVolumeAccessMode) []v1.PersistentVolumeAccessMode {\n\tset := []v1.PersistentVolumeAccessMode{}\n\tfor _, c := range cc {\n\t\tif !accessContains(set, c) {\n\t\t\tset = append(set, c)\n\t\t}\n\t}\n\n\treturn set\n}\n"
  },
  {
    "path": "internal/render/pv_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPersistentVolumeRender(t *testing.T) {\n\tc := render.PersistentVolume{}\n\tr := model1.NewRow(9)\n\n\trequire.NoError(t, c.Render(load(t, \"pv\"), \"-\", &r))\n\tassert.Equal(t, \"-/pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b\", r.ID)\n\tassert.Equal(t, model1.Fields{\"pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b\", \"1Gi\", \"RWO\", \"Delete\", \"Bound\", \"default/www-nginx-sts-1\", \"standard\"}, r.Fields[:7])\n}\n\nfunc TestTerminatingPersistentVolumeRender(t *testing.T) {\n\tc := render.PersistentVolume{}\n\tr := model1.NewRow(9)\n\n\trequire.NoError(t, c.Render(load(t, \"pv_terminating\"), \"-\", &r))\n\tassert.Equal(t, \"-/pvc-a4d86f51-916c-476b-83af-b551c91a8ac0\", r.ID)\n\tassert.Equal(t, model1.Fields{\"pvc-a4d86f51-916c-476b-83af-b551c91a8ac0\", \"1Gi\", \"RWO\", \"Delete\", \"Terminating\", \"default/www-nginx-sts-2\", \"standard\"}, r.Fields[:7])\n}\n"
  },
  {
    "path": "internal/render/pvc.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar defaultPVCHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"STATUS\"},\n\tmodel1.HeaderColumn{Name: \"VOLUME\"},\n\tmodel1.HeaderColumn{Name: \"CAPACITY\", Attrs: model1.Attrs{Capacity: true}},\n\tmodel1.HeaderColumn{Name: \"ACCESS MODES\"},\n\tmodel1.HeaderColumn{Name: \"STORAGECLASS\"},\n\tmodel1.HeaderColumn{Name: \"LABELS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// PersistentVolumeClaim renders a K8s PersistentVolumeClaim to screen.\ntype PersistentVolumeClaim struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (p PersistentVolumeClaim) Header(_ string) model1.Header {\n\treturn p.doHeader(defaultPVCHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (p PersistentVolumeClaim) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\n\tif err := p.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif p.specs.isEmpty() {\n\t\treturn nil\n\t}\n\n\tcols, err := p.specs.realize(raw, defaultPVCHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\nfunc (p PersistentVolumeClaim) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {\n\tvar pvc v1.PersistentVolumeClaim\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &pvc)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tphase := pvc.Status.Phase\n\tif pvc.DeletionTimestamp != nil {\n\t\tphase = \"Terminating\"\n\t}\n\n\tstorage := pvc.Spec.Resources.Requests[v1.ResourceStorage]\n\tvar capacity, accessModes string\n\tif pvc.Spec.VolumeName != \"\" {\n\t\taccessModes = accessMode(pvc.Status.AccessModes)\n\t\tstorage = pvc.Status.Capacity[v1.ResourceStorage]\n\t\tcapacity = storage.String()\n\t}\n\tclass, found := pvc.Annotations[v1.BetaStorageClassAnnotation]\n\tif !found {\n\t\tif pvc.Spec.StorageClassName != nil {\n\t\t\tclass = *pvc.Spec.StorageClassName\n\t\t}\n\t}\n\n\tr.ID = client.MetaFQN(&pvc.ObjectMeta)\n\tr.Fields = model1.Fields{\n\t\tpvc.Namespace,\n\t\tpvc.Name,\n\t\tstring(phase),\n\t\tpvc.Spec.VolumeName,\n\t\tcapacity,\n\t\taccessModes,\n\t\tclass,\n\t\tmapToStr(pvc.Labels),\n\t\tAsStatus(p.diagnose(string(phase))),\n\t\tToAge(pvc.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n\nfunc (PersistentVolumeClaim) diagnose(r string) error {\n\tif r != \"Bound\" && r != \"Available\" {\n\t\treturn fmt.Errorf(\"unexpected status %s\", r)\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/render/pvc_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPersistentVolumeClaimRender(t *testing.T) {\n\tc := render.PersistentVolumeClaim{}\n\tr := model1.NewRow(8)\n\n\trequire.NoError(t, c.Render(load(t, \"pvc\"), \"\", &r))\n\tassert.Equal(t, \"default/www-nginx-sts-0\", r.ID)\n\tassert.Equal(t, model1.Fields{\"default\", \"www-nginx-sts-0\", \"Bound\", \"pvc-fbabd470-8725-11e9-a8e8-42010a80015b\", \"1Gi\", \"RWO\", \"standard\"}, r.Fields[:7])\n}\n"
  },
  {
    "path": "internal/render/rbac.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n)\n\nconst allVerbs = \"*\"\n\nvar (\n\tk8sVerbs = []string{\n\t\t\"get\",\n\t\t\"list\",\n\t\t\"watch\",\n\t\t\"create\",\n\t\t\"patch\",\n\t\t\"update\",\n\t\t\"delete\",\n\t\t\"deletecollection\",\n\t}\n\n\thttpTok8sVerbs = map[string]string{\n\t\t\"post\": \"create\",\n\t\t\"put\":  \"update\",\n\t}\n)\n\n// Rbac renders a rbac to screen.\ntype Rbac struct {\n\tBase\n}\n\n// ColorerFunc colors a resource row.\nfunc (Rbac) ColorerFunc() model1.ColorerFunc {\n\treturn model1.DefaultColorer\n}\n\n// Header returns a header row.\nfunc (Rbac) Header(string) model1.Header {\n\th := make(model1.Header, 0, 10)\n\th = append(h,\n\t\tmodel1.HeaderColumn{Name: \"NAME\"},\n\t\tmodel1.HeaderColumn{Name: \"API-GROUP\"},\n\t)\n\th = append(h, rbacVerbHeader()...)\n\n\treturn append(h, model1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}})\n}\n\n// Render renders a K8s resource to screen.\nfunc (r Rbac) Render(o any, ns string, ro *model1.Row) error {\n\tp, ok := o.(*PolicyRes)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting PolicyRes but got %T\", o)\n\t}\n\n\tro.ID = p.Resource\n\tro.Fields = make(model1.Fields, 0, len(r.Header(ns)))\n\tro.Fields = append(ro.Fields,\n\t\tcleanseResource(p.Resource),\n\t\tp.Group,\n\t)\n\tro.Fields = append(ro.Fields, asVerbs(p.Verbs)...)\n\tro.Fields = append(ro.Fields, \"\")\n\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc asVerbs(verbs []string) []string {\n\tconst (\n\t\tverbLen    = 4\n\t\tunknownLen = 30\n\t)\n\n\tr := make([]string, 0, len(k8sVerbs)+1)\n\tfor _, v := range k8sVerbs {\n\t\tr = append(r, toVerbIcon(hasVerb(verbs, v)))\n\t}\n\n\tvar unknowns []string\n\tfor _, v := range verbs {\n\t\tif hv, ok := httpTok8sVerbs[v]; ok {\n\t\t\tv = hv\n\t\t}\n\t\tif !hasVerb(k8sVerbs, v) && v != allVerbs {\n\t\t\tunknowns = append(unknowns, v)\n\t\t}\n\t}\n\n\treturn append(r, Truncate(strings.Join(unknowns, \",\"), unknownLen))\n}\n\nfunc toVerbIcon(ok bool) string {\n\tif ok {\n\t\treturn \"[green::b] ✓ [::]\"\n\t}\n\treturn \"[orangered::b] × [::]\"\n}\n\nfunc hasVerb(verbs []string, verb string) bool {\n\tif len(verbs) == 1 && verbs[0] == allVerbs {\n\t\treturn true\n\t}\n\n\tfor _, v := range verbs {\n\t\tif hv, ok := httpTok8sVerbs[v]; ok {\n\t\t\tif hv == verb {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\tif v == verb {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// RuleRes represents an rbac rule.\ntype RuleRes struct {\n\tResource, Group string\n\tResourceName    string\n\tNonResourceURL  string\n\tVerbs           []string\n}\n\n// NewRuleRes returns a new rule.\nfunc NewRuleRes(res, grp string, vv []string) *RuleRes {\n\treturn &RuleRes{\n\t\tResource: res,\n\t\tGroup:    grp,\n\t\tVerbs:    vv,\n\t}\n}\n\n// GetObjectKind returns a schema object.\nfunc (*RuleRes) GetObjectKind() schema.ObjectKind {\n\treturn nil\n}\n\n// DeepCopyObject returns a container copy.\nfunc (r *RuleRes) DeepCopyObject() runtime.Object {\n\treturn r\n}\n\n// Rules represents a collection of rules.\ntype Rules []*RuleRes\n\n// Upsert adds a new rule.\nfunc (rr Rules) Upsert(r *RuleRes) Rules {\n\tidx, ok := rr.find(r.Resource)\n\tif !ok {\n\t\treturn append(rr, r)\n\t}\n\trr[idx] = r\n\n\treturn rr\n}\n\n// Find locates a row by id. Returns false is not found.\nfunc (rr Rules) find(res string) (int, bool) {\n\tfor i, r := range rr {\n\t\tif r.Resource == res {\n\t\t\treturn i, true\n\t\t}\n\t}\n\n\treturn 0, false\n}\n"
  },
  {
    "path": "internal/render/reference.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n)\n\n// Reference renders a reference to screen.\ntype Reference struct {\n\tBase\n}\n\n// ColorerFunc colors a resource row.\nfunc (Reference) ColorerFunc() model1.ColorerFunc {\n\treturn func(string, model1.Header, *model1.RowEvent) tcell.Color {\n\t\treturn tcell.ColorCadetBlue\n\t}\n}\n\n// Header returns a header row.\nfunc (Reference) Header(string) model1.Header {\n\treturn model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\t\tmodel1.HeaderColumn{Name: \"NAME\"},\n\t\tmodel1.HeaderColumn{Name: \"GVR\"},\n\t}\n}\n\n// Render renders a K8s resource to screen.\n// BOZO!! Pass in a row with pre-alloc fields??\nfunc (Reference) Render(o any, _ string, r *model1.Row) error {\n\tref, ok := o.(ReferenceRes)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected ReferenceRes, but got %T\", o)\n\t}\n\n\tr.ID = client.FQN(ref.Namespace, ref.Name)\n\tr.Fields = append(r.Fields,\n\t\tref.Namespace,\n\t\tref.Name,\n\t\tref.GVR,\n\t)\n\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\n// ReferenceRes represents a reference resource.\ntype ReferenceRes struct {\n\tNamespace string\n\tName      string\n\tGVR       string\n}\n\n// GetObjectKind returns a schema object.\nfunc (ReferenceRes) GetObjectKind() schema.ObjectKind {\n\treturn nil\n}\n\n// DeepCopyObject returns a container copy.\nfunc (a ReferenceRes) DeepCopyObject() runtime.Object {\n\treturn a\n}\n"
  },
  {
    "path": "internal/render/reference_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestReferenceRender(t *testing.T) {\n\to := render.ReferenceRes{\n\t\tNamespace: \"ns1\",\n\t\tName:      \"blee\",\n\t\tGVR:       client.SecGVR.String(),\n\t}\n\n\tvar (\n\t\tref = render.Reference{}\n\t\tr   model1.Row\n\t)\n\trequire.NoError(t, ref.Render(o, \"fred\", &r))\n\tassert.Equal(t, \"ns1/blee\", r.ID)\n\tassert.Equal(t, model1.Fields{\n\t\t\"ns1\",\n\t\t\"blee\",\n\t\tclient.SecGVR.String(),\n\t}, r.Fields)\n}\n"
  },
  {
    "path": "internal/render/render_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n)\n\n// Helpers...\n\nfunc load(t testing.TB, n string) *unstructured.Unstructured {\n\traw, err := os.ReadFile(fmt.Sprintf(\"testdata/%s.json\", n))\n\trequire.NoError(t, err)\n\n\tvar o unstructured.Unstructured\n\terr = json.Unmarshal(raw, &o)\n\trequire.NoError(t, err)\n\n\treturn &o\n}\n"
  },
  {
    "path": "internal/render/ro.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\trbacv1 \"k8s.io/api/rbac/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\n// Role renders a K8s Role to screen.\ntype Role struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (r Role) Header(_ string) model1.Header {\n\treturn r.doHeader(defaultROHeader)\n}\n\nvar defaultROHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"LABELS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// Render renders a K8s resource to screen.\nfunc (r Role) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tif err := r.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif r.specs.isEmpty() {\n\t\treturn nil\n\t}\n\tcols, err := r.specs.realize(raw, defaultROHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\nfunc (Role) defaultRow(raw *unstructured.Unstructured, row *model1.Row) error {\n\tvar ro rbacv1.Role\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ro)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trow.ID = client.MetaFQN(&ro.ObjectMeta)\n\trow.Fields = model1.Fields{\n\t\tro.Namespace,\n\t\tro.Name,\n\t\tmapToStr(ro.Labels),\n\t\t\"\",\n\t\tToAge(ro.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/render/ro_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRoleRender(t *testing.T) {\n\tc := render.Role{}\n\tr := model1.NewRow(3)\n\n\trequire.NoError(t, c.Render(load(t, \"ro\"), \"\", &r))\n\tassert.Equal(t, \"default/blee\", r.ID)\n\tassert.Equal(t, model1.Fields{\"default\", \"blee\"}, r.Fields[:2])\n}\n"
  },
  {
    "path": "internal/render/rob.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\trbacv1 \"k8s.io/api/rbac/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar defaultROBHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"ROLE\"},\n\tmodel1.HeaderColumn{Name: \"KIND\"},\n\tmodel1.HeaderColumn{Name: \"SUBJECTS\"},\n\tmodel1.HeaderColumn{Name: \"LABELS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// RoleBinding renders a K8s RoleBinding to screen.\ntype RoleBinding struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (r RoleBinding) Header(_ string) model1.Header {\n\treturn r.doHeader(defaultROBHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (r RoleBinding) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tif err := r.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif r.specs.isEmpty() {\n\t\treturn nil\n\t}\n\tcols, err := r.specs.realize(raw, defaultROBHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\nfunc (RoleBinding) defaultRow(raw *unstructured.Unstructured, row *model1.Row) error {\n\tvar rb rbacv1.RoleBinding\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &rb)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tkind, ss := renderSubjects(rb.Subjects)\n\n\trow.ID = client.MetaFQN(&rb.ObjectMeta)\n\trow.Fields = model1.Fields{\n\t\trb.Namespace,\n\t\trb.Name,\n\t\trb.RoleRef.Name,\n\t\tkind,\n\t\tss,\n\t\tmapToStr(rb.Labels),\n\t\t\"\",\n\t\tToAge(rb.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc renderSubjects(ss []rbacv1.Subject) (kind, subjects string) {\n\tif len(ss) == 0 {\n\t\treturn NAValue, \"\"\n\t}\n\n\ttt := make([]string, 0, len(ss))\n\tfor _, s := range ss {\n\t\tkind = toSubjectAlias(s.Kind)\n\t\ttt = append(tt, s.Name)\n\t}\n\treturn kind, strings.Join(tt, \",\")\n}\n\nfunc toSubjectAlias(s string) string {\n\tif s == \"\" {\n\t\treturn s\n\t}\n\n\tswitch s {\n\tcase rbacv1.UserKind:\n\t\treturn \"User\"\n\tcase rbacv1.GroupKind:\n\t\treturn \"Group\"\n\tcase rbacv1.ServiceAccountKind:\n\t\treturn \"SvcAcct\"\n\tdefault:\n\t\treturn strings.ToUpper(s)\n\t}\n}\n"
  },
  {
    "path": "internal/render/rob_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRoleBindingRender(t *testing.T) {\n\tc := render.RoleBinding{}\n\tr := model1.NewRow(6)\n\n\trequire.NoError(t, c.Render(load(t, \"rb\"), \"\", &r))\n\tassert.Equal(t, \"default/blee\", r.ID)\n\tassert.Equal(t, model1.Fields{\"default\", \"blee\", \"blee\", \"SvcAcct\", \"fernand\"}, r.Fields[:5])\n}\n"
  },
  {
    "path": "internal/render/rs.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/tview\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\n// ReplicaSet renders a K8s ReplicaSet to screen.\ntype ReplicaSet struct {\n\tBase\n}\n\n// ColorerFunc colors a resource row.\nfunc (ReplicaSet) ColorerFunc() model1.ColorerFunc {\n\treturn model1.DefaultColorer\n}\n\n// Header returns a header row.\nfunc (r ReplicaSet) Header(_ string) model1.Header {\n\treturn r.doHeader(defaultRSHeader)\n}\n\nvar defaultRSHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"VS\", Attrs: model1.Attrs{VS: true}},\n\tmodel1.HeaderColumn{Name: \"DESIRED\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"CURRENT\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"READY\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\tmodel1.HeaderColumn{Name: \"CONTAINERS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"IMAGES\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"SELECTOR\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// Render renders a K8s resource to screen.\nfunc (r ReplicaSet) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tif err := r.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif r.specs.isEmpty() {\n\t\treturn nil\n\t}\n\tcols, err := r.specs.realize(raw, defaultRSHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\nfunc (r ReplicaSet) defaultRow(raw *unstructured.Unstructured, row *model1.Row) error {\n\tvar rs appsv1.ReplicaSet\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &rs)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar (\n\t\tcc        = rs.Spec.Template.Spec.Containers\n\t\tcos, imgs = make([]string, 0, len(cc)), make([]string, 0, len(cc))\n\t)\n\tfor i := range cc {\n\t\tcos, imgs = append(cos, cc[i].Name), append(imgs, cc[i].Image)\n\t}\n\n\trow.ID = client.MetaFQN(&rs.ObjectMeta)\n\trow.Fields = model1.Fields{\n\t\trs.Namespace,\n\t\trs.Name,\n\t\tcomputeVulScore(rs.Namespace, rs.Labels, &rs.Spec.Template.Spec),\n\t\tstrconv.Itoa(int(*rs.Spec.Replicas)),\n\t\tstrconv.Itoa(int(rs.Status.Replicas)),\n\t\tstrconv.Itoa(int(rs.Status.ReadyReplicas)),\n\t\tstrings.Join(cos, \",\"),\n\t\tstrings.Join(imgs, \",\"),\n\t\tmapToStr(rs.Labels),\n\t\tAsStatus(r.diagnose(&rs)),\n\t\tToAge(rs.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n\nfunc (ReplicaSet) diagnose(rs *appsv1.ReplicaSet) error {\n\tif rs.Status.Replicas != rs.Status.ReadyReplicas {\n\t\tif rs.Status.Replicas == 0 {\n\t\t\treturn fmt.Errorf(\"did not phase down correctly expecting 0 replicas but got %d\", rs.Status.ReadyReplicas)\n\t\t}\n\t\treturn fmt.Errorf(\"mismatch desired(%d) vs ready(%d)\", rs.Status.Replicas, rs.Status.ReadyReplicas)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/render/rs_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestReplicaSetRender(t *testing.T) {\n\tc := render.ReplicaSet{}\n\tr := model1.NewRow(4)\n\n\trequire.NoError(t, c.Render(load(t, \"rs\"), \"\", &r))\n\tassert.Equal(t, \"icx/icx-db-7d4b578979\", r.ID)\n\tassert.Equal(t, model1.Fields{\"icx\", \"icx-db-7d4b578979\", \"n/a\", \"1\", \"1\", \"1\"}, r.Fields[:6])\n}\n"
  },
  {
    "path": "internal/render/sa.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar defaultSAHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"SECRET\"},\n\tmodel1.HeaderColumn{Name: \"LABELS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// ServiceAccount renders a K8s ServiceAccount to screen.\ntype ServiceAccount struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (s ServiceAccount) Header(_ string) model1.Header {\n\treturn s.doHeader(defaultSAHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (s ServiceAccount) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tif err := s.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif s.specs.isEmpty() {\n\t\treturn nil\n\t}\n\tcols, err := s.specs.realize(raw, defaultSAHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\nfunc (ServiceAccount) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {\n\tvar sa v1.ServiceAccount\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sa)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tr.ID = client.MetaFQN(&sa.ObjectMeta)\n\tr.Fields = model1.Fields{\n\t\tsa.Namespace,\n\t\tsa.Name,\n\t\tstrconv.Itoa(len(sa.Secrets)),\n\t\tmapToStr(sa.Labels),\n\t\t\"\",\n\t\tToAge(sa.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/render/sa_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestServiceAccountRender(t *testing.T) {\n\tc := render.ServiceAccount{}\n\tr := model1.NewRow(4)\n\n\trequire.NoError(t, c.Render(load(t, \"sa\"), \"\", &r))\n\tassert.Equal(t, \"default/blee\", r.ID)\n\tassert.Equal(t, model1.Fields{\"default\", \"blee\", \"2\"}, r.Fields[:3])\n}\n"
  },
  {
    "path": "internal/render/sc.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\tstoragev1 \"k8s.io/api/storage/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/kubectl/pkg/util/storage\"\n)\n\nvar defaultSCHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"PROVISIONER\"},\n\tmodel1.HeaderColumn{Name: \"RECLAIMPOLICY\"},\n\tmodel1.HeaderColumn{Name: \"VOLUMEBINDINGMODE\"},\n\tmodel1.HeaderColumn{Name: \"ALLOWVOLUMEEXPANSION\"},\n\tmodel1.HeaderColumn{Name: \"LABELS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// StorageClass renders a K8s StorageClass to screen.\ntype StorageClass struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (s StorageClass) Header(_ string) model1.Header {\n\treturn s.doHeader(defaultSCHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (s StorageClass) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tif err := s.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif s.specs.isEmpty() {\n\t\treturn nil\n\t}\n\tcols, err := s.specs.realize(raw, defaultSCHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\nfunc (s StorageClass) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {\n\tvar sc storagev1.StorageClass\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sc)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tr.ID = client.FQN(client.ClusterScope, sc.Name)\n\tr.Fields = model1.Fields{\n\t\ts.nameWithDefault(&sc.ObjectMeta),\n\t\tsc.Provisioner,\n\t\tstrPtrToStr((*string)(sc.ReclaimPolicy)),\n\t\tstrPtrToStr((*string)(sc.VolumeBindingMode)),\n\t\tboolPtrToStr(sc.AllowVolumeExpansion),\n\t\tmapToStr(sc.Labels),\n\t\t\"\",\n\t\tToAge(sc.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n\nfunc (StorageClass) nameWithDefault(meta *metav1.ObjectMeta) string {\n\tif storage.IsDefaultAnnotationText(*meta) == \"Yes\" {\n\t\treturn meta.Name + \" (default)\"\n\t}\n\treturn meta.Name\n}\n"
  },
  {
    "path": "internal/render/sc_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestStorageClassRender(t *testing.T) {\n\tc := render.StorageClass{}\n\tr := model1.NewRow(4)\n\n\trequire.NoError(t, c.Render(load(t, \"sc\"), \"\", &r))\n\tassert.Equal(t, \"-/standard\", r.ID)\n\tassert.Equal(t, model1.Fields{\"standard (default)\", \"kubernetes.io/gce-pd\", \"Delete\", \"Immediate\", \"true\"}, r.Fields[:5])\n}\n"
  },
  {
    "path": "internal/render/screen_dump.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n\t\"k8s.io/apimachinery/pkg/util/duration\"\n)\n\n// ScreenDump renders a screendumps to screen.\ntype ScreenDump struct {\n\tBase\n}\n\n// ColorerFunc colors a resource row.\nfunc (ScreenDump) ColorerFunc() model1.ColorerFunc {\n\treturn func(string, model1.Header, *model1.RowEvent) tcell.Color {\n\t\treturn tcell.ColorNavajoWhite\n\t}\n}\n\n// Header returns a header row.\nfunc (ScreenDump) Header(string) model1.Header {\n\treturn model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"NAME\"},\n\t\tmodel1.HeaderColumn{Name: \"DIR\"},\n\t\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\t\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n\t}\n}\n\n// Render renders a K8s resource to screen.\nfunc (ScreenDump) Render(o any, _ string, r *model1.Row) error {\n\tf, ok := o.(FileRes)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting screendumper, but got %T\", o)\n\t}\n\n\tr.ID = filepath.Join(f.Dir, f.File.Name())\n\tr.Fields = model1.Fields{\n\t\tf.File.Name(),\n\t\tf.Dir,\n\t\t\"\",\n\t\ttimeToAge(f.File.ModTime()),\n\t}\n\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc timeToAge(timestamp time.Time) string {\n\treturn duration.HumanDuration(time.Since(timestamp))\n}\n\n// FileRes represents a file resource.\ntype FileRes struct {\n\tFile os.FileInfo\n\tDir  string\n}\n\n// GetObjectKind returns a schema object.\nfunc (FileRes) GetObjectKind() schema.ObjectKind {\n\treturn nil\n}\n\n// DeepCopyObject returns a container copy.\nfunc (c FileRes) DeepCopyObject() runtime.Object {\n\treturn c\n}\n"
  },
  {
    "path": "internal/render/screen_dump_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestScreenDumpRender(t *testing.T) {\n\tvar s render.ScreenDump\n\tvar r model1.Row\n\to := render.FileRes{\n\t\tFile: fileInfo{},\n\t\tDir:  \"fred/blee\",\n\t}\n\n\trequire.NoError(t, s.Render(o, \"fred\", &r))\n\tassert.Equal(t, \"fred/blee/bob\", r.ID)\n\tassert.Equal(t, model1.Fields{\n\t\t\"bob\",\n\t\t\"fred/blee\",\n\t\t\"\",\n\t}, r.Fields[:len(r.Fields)-1])\n}\n\n// Helpers...\n\ntype fileInfo struct{}\n\nvar _ os.FileInfo = fileInfo{}\n\nfunc (fileInfo) Name() string       { return \"bob\" }\nfunc (fileInfo) Size() int64        { return 100 }\nfunc (fileInfo) ModTime() time.Time { return testTime() }\nfunc (fileInfo) IsDir() bool        { return false }\nfunc (fileInfo) Sys() any           { return nil }\n\nfunc (fileInfo) Mode() os.FileMode {\n\treturn os.FileMode(0644)\n}\n"
  },
  {
    "path": "internal/render/secret.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar defaultSECHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"TYPE\"},\n\tmodel1.HeaderColumn{Name: \"DATA\"},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// Secret renders a K8s Secret to screen.\ntype Secret struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (s Secret) Header(_ string) model1.Header {\n\treturn s.doHeader(defaultSECHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (s Secret) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tif err := s.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif s.specs.isEmpty() {\n\t\treturn nil\n\t}\n\tcols, err := s.specs.realize(raw, defaultSECHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\nfunc (Secret) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {\n\tvar sec v1.Secret\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sec)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tr.ID = client.FQN(sec.Namespace, sec.Name)\n\tr.Fields = model1.Fields{\n\t\tsec.Namespace,\n\t\tsec.Name,\n\t\tstring(sec.Type),\n\t\tstrconv.Itoa(len(sec.Data)),\n\t\t\"\",\n\t\tToAge(raw.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/render/section.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\n// Level tracks lint check level.\ntype Level int\n\nconst (\n\t// OkLevel denotes no linting issues.\n\tOkLevel Level = iota\n\t// InfoLevel denotes FIY linting issues.\n\tInfoLevel\n\t// WarnLevel denotes a warning issue.\n\tWarnLevel\n\t// ErrorLevel denotes a serious issue.\n\tErrorLevel\n)\n\ntype (\n\t// Sections represents a collection of sections.\n\tSections []Section\n\n\t// Section represents a sanitizer pass.\n\tSection struct {\n\t\tTitle   string  `json:\"sanitizer\" yaml:\"sanitizer\"`\n\t\tGVR     string  `yaml:\"gvr\" json:\"gvr\"`\n\t\tOutcome Outcome `json:\"issues,omitempty\" yaml:\"issues,omitempty\"`\n\t}\n\n\t// Outcome represents a classification of reports outcome.\n\tOutcome map[string]Issues\n\n\t// Issues represents a collection of issues.\n\tIssues []Issue\n\n\t// Issue represents a sanitization issue.\n\tIssue struct {\n\t\tGroup   string `yaml:\"group\" json:\"group\"`\n\t\tGVR     string `yaml:\"gvr\" json:\"gvr\"`\n\t\tLevel   Level  `yaml:\"level\" json:\"level\"`\n\t\tMessage string `yaml:\"message\" json:\"message\"`\n\t}\n)\n"
  },
  {
    "path": "internal/render/sts.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nvar defaultSTSHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"VS\", Attrs: model1.Attrs{VS: true}},\n\tmodel1.HeaderColumn{Name: \"READY\"},\n\tmodel1.HeaderColumn{Name: \"SELECTOR\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"SERVICE\"},\n\tmodel1.HeaderColumn{Name: \"CONTAINERS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"IMAGES\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"LABELS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// StatefulSet renders a K8s StatefulSet to screen.\ntype StatefulSet struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (s StatefulSet) Header(_ string) model1.Header {\n\treturn s.doHeader(defaultSTSHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (s StatefulSet) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tif err := s.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif s.specs.isEmpty() {\n\t\treturn nil\n\t}\n\tcols, err := s.specs.realize(raw, defaultSTSHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\nfunc (s StatefulSet) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {\n\tvar sts appsv1.StatefulSet\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar desired int32\n\tif sts.Spec.Replicas != nil {\n\t\tdesired = *sts.Spec.Replicas\n\t}\n\tr.ID = client.MetaFQN(&sts.ObjectMeta)\n\tr.Fields = model1.Fields{\n\t\tsts.Namespace,\n\t\tsts.Name,\n\t\tcomputeVulScore(sts.Namespace, sts.Labels, &sts.Spec.Template.Spec),\n\t\tstrconv.Itoa(int(sts.Status.ReadyReplicas)) + \"/\" + strconv.Itoa(int(desired)),\n\t\tasSelector(sts.Spec.Selector),\n\t\tna(sts.Spec.ServiceName),\n\t\tpodContainerNames(&sts.Spec.Template.Spec, true),\n\t\tpodImageNames(&sts.Spec.Template.Spec, true),\n\t\tmapToStr(sts.Labels),\n\t\tAsStatus(s.diagnose(desired, sts.Status.Replicas, sts.Status.ReadyReplicas)),\n\t\tToAge(sts.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n\nfunc (StatefulSet) diagnose(d, c, r int32) error {\n\tif c != r {\n\t\treturn fmt.Errorf(\"desired %d replicas got %d available\", c, r)\n\t}\n\tif d != r {\n\t\treturn fmt.Errorf(\"want %d replicas got %d available\", d, r)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/render/sts_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestStatefulSetRender(t *testing.T) {\n\tc := render.StatefulSet{}\n\tr := model1.NewRow(4)\n\n\trequire.NoError(t, c.Render(load(t, \"sts\"), \"\", &r))\n\tassert.Equal(t, \"default/nginx-sts\", r.ID)\n\tassert.Equal(t, model1.Fields{\"default\", \"nginx-sts\", \"n/a\", \"4/4\", \"app=nginx-sts\", \"nginx-sts\", \"nginx\", \"k8s.gcr.io/nginx-slim:0.8\", \"app=nginx-sts\", \"\"}, r.Fields[:len(r.Fields)-1])\n}\n"
  },
  {
    "path": "internal/render/subject.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n)\n\n// Subject renders a rbac to screen.\ntype Subject struct {\n\tBase\n}\n\n// ColorerFunc colors a resource row.\nfunc (Subject) ColorerFunc() model1.ColorerFunc {\n\treturn func(string, model1.Header, *model1.RowEvent) tcell.Color {\n\t\treturn tcell.ColorMediumSpringGreen\n\t}\n}\n\n// Header returns a header row.\nfunc (Subject) Header(string) model1.Header {\n\treturn model1.Header{\n\t\tmodel1.HeaderColumn{Name: \"NAME\"},\n\t\tmodel1.HeaderColumn{Name: \"KIND\"},\n\t\tmodel1.HeaderColumn{Name: \"FIRST LOCATION\"},\n\t\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\t}\n}\n\n// Render renders a K8s resource to screen.\nfunc (s Subject) Render(o any, _ string, r *model1.Row) error {\n\tres, ok := o.(SubjectRes)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected SubjectRes, but got %T\", s)\n\t}\n\n\tr.ID = res.Name\n\tr.Fields = model1.Fields{\n\t\tres.Name,\n\t\tres.Kind,\n\t\tres.FirstLocation,\n\t\t\"\",\n\t}\n\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\n// SubjectRes represents a subject rule.\ntype SubjectRes struct {\n\tName, Kind, FirstLocation string\n}\n\n// GetObjectKind returns a schema object.\nfunc (SubjectRes) GetObjectKind() schema.ObjectKind {\n\treturn nil\n}\n\n// DeepCopyObject returns a container copy.\nfunc (s SubjectRes) DeepCopyObject() runtime.Object {\n\treturn s\n}\n\n// Subjects represents a collection of RBAC policies.\ntype Subjects []SubjectRes\n\n// Upsert adds a new subject.\nfunc (ss Subjects) Upsert(s SubjectRes) Subjects {\n\tidx, ok := ss.find(s.Name)\n\tif !ok {\n\t\treturn append(ss, s)\n\t}\n\tss[idx] = s\n\n\treturn ss\n}\n\n// Find locates a row by id. Returns false is not found.\nfunc (ss Subjects) find(res string) (int, bool) {\n\tfor i, s := range ss {\n\t\tif s.Name == res {\n\t\t\treturn i, true\n\t\t}\n\t}\n\n\treturn 0, false\n}\n"
  },
  {
    "path": "internal/render/svc.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\n// Header returns a header row.\nvar defaultSVCHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"TYPE\"},\n\tmodel1.HeaderColumn{Name: \"CLUSTER-IP\"},\n\tmodel1.HeaderColumn{Name: \"EXTERNAL-IP\"},\n\tmodel1.HeaderColumn{Name: \"SELECTOR\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"PORTS\", Attrs: model1.Attrs{Wide: false}},\n\tmodel1.HeaderColumn{Name: \"LABELS\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// Service renders a K8s Service to screen.\ntype Service struct {\n\tBase\n}\n\n// Header returns a header row.\nfunc (s Service) Header(_ string) model1.Header {\n\treturn s.doHeader(defaultSVCHeader)\n}\n\n// Render renders a K8s resource to screen.\nfunc (s Service) Render(o any, _ string, row *model1.Row) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tif err := s.defaultRow(raw, row); err != nil {\n\t\treturn err\n\t}\n\tif s.specs.isEmpty() {\n\t\treturn nil\n\t}\n\tcols, err := s.specs.realize(raw, defaultSVCHeader, row)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(row)\n\n\treturn nil\n}\n\nfunc (s Service) defaultRow(raw *unstructured.Unstructured, r *model1.Row) error {\n\tvar svc v1.Service\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &svc)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tr.ID = client.MetaFQN(&svc.ObjectMeta)\n\tr.Fields = model1.Fields{\n\t\tsvc.Namespace,\n\t\tsvc.Name,\n\t\tstring(svc.Spec.Type),\n\t\ttoIP(svc.Spec.ClusterIP),\n\t\ttoIPs(svc.Spec.Type, getSvcExtIPS(&svc)),\n\t\tmapToStr(svc.Spec.Selector),\n\t\tToPorts(svc.Spec.Ports),\n\t\tmapToStr(svc.Labels),\n\t\tAsStatus(s.diagnose()),\n\t\tToAge(svc.GetCreationTimestamp()),\n\t}\n\n\treturn nil\n}\n\nfunc (Service) diagnose() error {\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc toIP(ip string) string {\n\tif ip == \"\" || ip == \"None\" {\n\t\treturn \"\"\n\t}\n\treturn ip\n}\n\nfunc getSvcExtIPS(svc *v1.Service) []string {\n\tresults := []string{}\n\n\tswitch svc.Spec.Type {\n\tcase v1.ServiceTypeNodePort, v1.ServiceTypeClusterIP:\n\t\treturn svc.Spec.ExternalIPs\n\tcase v1.ServiceTypeLoadBalancer:\n\t\tlbIps := lbIngressIP(svc.Status.LoadBalancer)\n\t\tif len(svc.Spec.ExternalIPs) > 0 {\n\t\t\tif lbIps != \"\" {\n\t\t\t\tresults = append(results, lbIps)\n\t\t\t}\n\t\t\treturn append(results, svc.Spec.ExternalIPs...)\n\t\t}\n\t\tif lbIps != \"\" {\n\t\t\tresults = append(results, lbIps)\n\t\t}\n\tcase v1.ServiceTypeExternalName:\n\t\tresults = append(results, svc.Spec.ExternalName)\n\t}\n\n\treturn results\n}\n\nfunc lbIngressIP(s v1.LoadBalancerStatus) string {\n\tingress := s.Ingress\n\tresult := []string{}\n\tfor i := range ingress {\n\t\tif ingress[i].IP != \"\" {\n\t\t\tresult = append(result, ingress[i].IP)\n\t\t} else if ingress[i].Hostname != \"\" {\n\t\t\tresult = append(result, ingress[i].Hostname)\n\t\t}\n\t}\n\n\treturn strings.Join(result, \",\")\n}\n\nfunc toIPs(svcType v1.ServiceType, ips []string) string {\n\tif len(ips) == 0 {\n\t\tif svcType == v1.ServiceTypeLoadBalancer {\n\t\t\treturn \"<pending>\"\n\t\t}\n\t\treturn \"\"\n\t}\n\tsort.Strings(ips)\n\n\treturn strings.Join(ips, \",\")\n}\n\n// ToPorts returns service ports as a string.\nfunc ToPorts(pp []v1.ServicePort) string {\n\tports := make([]string, len(pp))\n\tfor i, p := range pp {\n\t\tif p.Name != \"\" {\n\t\t\tports[i] = p.Name + \":\"\n\t\t}\n\t\tports[i] += strconv.Itoa(int(p.Port)) +\n\t\t\t\"►\" +\n\t\t\tstrconv.Itoa(int(p.NodePort))\n\t\tif p.Protocol != \"TCP\" {\n\t\t\tports[i] += \"╱\" + string(p.Protocol)\n\t\t}\n\t}\n\n\treturn strings.Join(ports, \" \")\n}\n"
  },
  {
    "path": "internal/render/svc_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestServiceRender(t *testing.T) {\n\tc := render.Service{}\n\tr := model1.NewRow(4)\n\n\trequire.NoError(t, c.Render(load(t, \"svc\"), \"\", &r))\n\tassert.Equal(t, \"default/dictionary1\", r.ID)\n\tassert.Equal(t, model1.Fields{\"default\", \"dictionary1\", \"ClusterIP\", \"10.47.248.116\", \"\", \"app=dictionary1\", \"http:4001►0\"}, r.Fields[:7])\n}\n\nfunc BenchmarkSvcRender(b *testing.B) {\n\tvar (\n\t\tsvc render.Service\n\t\tr   = model1.NewRow(4)\n\t\ts   = load(b, \"svc\")\n\t)\n\n\tb.ResetTimer()\n\tb.ReportAllocs()\n\tfor range b.N {\n\t\t_ = svc.Render(s, \"\", &r)\n\t}\n}\n"
  },
  {
    "path": "internal/render/table.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"k8s.io/apimachinery/pkg/api/meta\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/util/sets\"\n)\n\nconst ageTableCol = \"Age\"\n\nvar ageCols = sets.New(\"Last Seen\", \"First Seen\", \"Age\")\n\n// Table renders a tabular resource to screen.\ntype Table struct {\n\tBase\n\ttable    *metav1.Table\n\theader   model1.Header\n\tageIndex int\n\tmx       sync.RWMutex\n}\n\nfunc (*Table) IsGeneric() bool {\n\treturn true\n}\n\nfunc (t *Table) setAgeIndex(idx int) {\n\tt.mx.Lock()\n\tdefer t.mx.Unlock()\n\tt.ageIndex = idx\n}\n\nfunc (t *Table) getAgeIndex() int {\n\tt.mx.RLock()\n\tdefer t.mx.RUnlock()\n\treturn t.ageIndex\n}\n\n// SetTable sets the tabular resource.\nfunc (t *Table) SetTable(ns string, table *metav1.Table) {\n\tt.table = table\n\tt.header = t.Header(ns)\n}\n\n// ColorerFunc colors a resource row.\nfunc (*Table) ColorerFunc() model1.ColorerFunc {\n\treturn model1.DefaultColorer\n}\n\n// Header returns a header row.\nfunc (t *Table) Header(string) model1.Header {\n\treturn t.doHeader(t.defaultHeader())\n}\n\n// Header returns a header row.\nfunc (t *Table) defaultHeader() model1.Header {\n\tif t.table == nil {\n\t\treturn model1.Header{}\n\t}\n\th := make(model1.Header, 0, len(t.table.ColumnDefinitions))\n\tfor i, c := range t.table.ColumnDefinitions {\n\t\tif c.Name == ageTableCol {\n\t\t\tt.setAgeIndex(i)\n\t\t\tcontinue\n\t\t}\n\t\ttimeCol := ageCols.Has(c.Name)\n\t\th = append(h, model1.HeaderColumn{Name: strings.ToUpper(c.Name), Attrs: model1.Attrs{Time: timeCol}})\n\t}\n\tif t.getAgeIndex() > 0 {\n\t\th = append(h, model1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}})\n\t}\n\n\treturn h\n}\n\n// Render renders a K8s resource to screen.\nfunc (t *Table) Render(o any, ns string, r *model1.Row) error {\n\trow, ok := o.(metav1.TableRow)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected TableRow, but got %T\", o)\n\t}\n\tif err := t.defaultRow(&row, ns, r); err != nil {\n\t\treturn err\n\t}\n\tif t.specs.isEmpty() {\n\t\treturn nil\n\t}\n\n\tobj := row.Object.Object\n\tif obj != nil {\n\t\tobj = obj.DeepCopyObject()\n\t}\n\tcols, err := t.specs.realize(obj, t.defaultHeader(), r)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcols.hydrateRow(r)\n\n\treturn nil\n}\n\nfunc (t *Table) defaultRow(row *metav1.TableRow, ns string, r *model1.Row) error {\n\tth := t.header\n\tons, name := ns, UnknownValue\n\tswitch {\n\tcase row.Object.Object != nil:\n\t\tif m, _ := meta.Accessor(row.Object.Object); m != nil {\n\t\t\tons, name = m.GetNamespace(), m.GetName()\n\t\t}\n\tcase row.Object.Raw != nil:\n\t\tvar pm metav1.PartialObjectMetadata\n\t\tif err := json.Unmarshal(row.Object.Raw, &pm); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tons, name = pm.Namespace, pm.Name\n\tdefault:\n\t\tif idx, ok := th.IndexOf(\"NAME\", true); ok && idx >= 0 && idx < len(row.Cells) {\n\t\t\tname = row.Cells[idx].(string)\n\t\t}\n\t\tif idx, ok := th.IndexOf(\"NAMESPACE\", true); ok && idx >= 0 && idx < len(row.Cells) {\n\t\t\tons = row.Cells[idx].(string)\n\t\t}\n\t}\n\n\tif client.IsClusterWide(ons) {\n\t\tons = client.ClusterScope\n\t}\n\tr.ID = client.FQN(ons, name)\n\tr.Fields = make(model1.Fields, 0, len(th))\n\tvar (\n\t\tage    any\n\t\tageIdx = t.getAgeIndex()\n\t)\n\tfor i, c := range row.Cells {\n\t\tif ageIdx > 0 && i == ageIdx {\n\t\t\tage = c\n\t\t\tcontinue\n\t\t}\n\t\tif c == nil {\n\t\t\tr.Fields = append(r.Fields, Blank)\n\t\t\tcontinue\n\t\t}\n\t\tr.Fields = append(r.Fields, fmt.Sprintf(\"%v\", c))\n\t}\n\tif d, ok := age.(string); ok {\n\t\tr.Fields = append(r.Fields, d)\n\t} else if ageIdx > 0 {\n\t\tslog.Warn(\"No Duration detected on age field\")\n\t\tr.Fields = append(r.Fields, NAValue)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/render/table_int_test.go",
    "content": "package render\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/stretchr/testify/assert\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nfunc Test_defaultHeader(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcdefs []metav1.TableColumnDefinition\n\t\te     model1.Header\n\t}{\n\t\t\"empty\": {\n\t\t\te: make(model1.Header, 0),\n\t\t},\n\n\t\t\"plain\": {\n\t\t\tcdefs: []metav1.TableColumnDefinition{\n\t\t\t\t{Name: \"A\"},\n\t\t\t\t{Name: \"B\"},\n\t\t\t\t{Name: \"C\"},\n\t\t\t},\n\t\t\te: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"B\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t\t\t},\n\t\t},\n\n\t\t\"age\": {\n\t\t\tcdefs: []metav1.TableColumnDefinition{\n\t\t\t\t{Name: \"Fred\"},\n\t\t\t\t{Name: \"Blee\"},\n\t\t\t\t{Name: \"Age\"},\n\t\t\t},\n\t\t\te: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"FRED\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"BLEE\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n\t\t\t},\n\t\t},\n\n\t\t\"time-cols\": {\n\t\t\tcdefs: []metav1.TableColumnDefinition{\n\t\t\t\t{Name: \"Last Seen\"},\n\t\t\t\t{Name: \"Fred\"},\n\t\t\t\t{Name: \"Blee\"},\n\t\t\t\t{Name: \"Age\"},\n\t\t\t\t{Name: \"First Seen\"},\n\t\t\t},\n\t\t\te: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"LAST SEEN\", Attrs: model1.Attrs{Time: true}},\n\t\t\t\tmodel1.HeaderColumn{Name: \"FRED\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"BLEE\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"FIRST SEEN\", Attrs: model1.Attrs{Time: true}},\n\t\t\t\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tvar ta Table\n\t\t\tta.SetTable(\"ns-1\", &metav1.Table{ColumnDefinitions: u.cdefs})\n\t\t\tassert.Equal(t, u.e, ta.defaultHeader())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/render/table_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\tcfg \"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\tmetav1beta1 \"k8s.io/apimachinery/pkg/apis/meta/v1beta1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nfunc TestGenericRender(t *testing.T) {\n\tuu := map[string]struct {\n\t\tns      string\n\t\ttable   *metav1beta1.Table\n\t\teID     string\n\t\teFields model1.Fields\n\t\teHeader model1.Header\n\t}{\n\t\t\"withNS\": {\n\t\t\tns:      \"ns1\",\n\t\t\ttable:   makeNSGeneric(),\n\t\t\teID:     \"ns1/fred\",\n\t\t\teFields: model1.Fields{\"ns1\", \"c1\", \"c2\", \"c3\"},\n\t\t\teHeader: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"B\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t\t\t},\n\t\t},\n\n\t\t\"all\": {\n\t\t\tns:      client.NamespaceAll,\n\t\t\ttable:   makeNSGeneric(),\n\t\t\teID:     \"ns1/fred\",\n\t\t\teFields: model1.Fields{\"ns1\", \"c1\", \"c2\", \"c3\"},\n\t\t\teHeader: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"B\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t\t\t},\n\t\t},\n\n\t\t\"clusterWide\": {\n\t\t\tns:      client.ClusterScope,\n\t\t\ttable:   makeNoNSGeneric(),\n\t\t\teID:     \"-/fred\",\n\t\t\teFields: model1.Fields{\"c1\", \"c2\", \"c3\"},\n\t\t\teHeader: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"B\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t\t\t},\n\t\t},\n\n\t\t\"age\": {\n\t\t\tns:      client.ClusterScope,\n\t\t\ttable:   makeAgeGeneric(),\n\t\t\teID:     \"-/fred\",\n\t\t\teFields: model1.Fields{\"c1\", \"c2\", \"2d\"},\n\t\t\teHeader: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tvar re render.Table\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tvar r model1.Row\n\t\t\tre.SetTable(u.ns, u.table)\n\n\t\t\tassert.Equal(t, u.eHeader, re.Header(u.ns))\n\t\t\trequire.NoError(t, re.Render(u.table.Rows[0], u.ns, &r))\n\t\t\tassert.Equal(t, u.eID, r.ID)\n\t\t\tassert.Equal(t, u.eFields, r.Fields)\n\t\t})\n\t}\n}\n\nfunc TestGenericCustRender(t *testing.T) {\n\tuu := map[string]struct {\n\t\tns      string\n\t\ttable   *metav1beta1.Table\n\t\tvs      cfg.ViewSetting\n\t\teID     string\n\t\teFields model1.Fields\n\t\teHeader model1.Header\n\t}{\n\t\t\"spec\": {\n\t\t\tns:    \"ns1\",\n\t\t\ttable: makeNSGeneric(),\n\t\t\tvs: cfg.ViewSetting{\n\t\t\t\tColumns: []string{\n\t\t\t\t\t\"NAMESPACE\",\n\t\t\t\t\t\"BLEE:.metadata.name\",\n\t\t\t\t\t\"ZORG:.metadata.namespace\",\n\t\t\t\t},\n\t\t\t},\n\t\t\teID:     \"ns1/fred\",\n\t\t\teFields: model1.Fields{\"ns1\", \"fred\", \"ns1\", \"c1\", \"c2\", \"c3\"},\n\t\t\teHeader: model1.Header{\n\t\t\t\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"BLEE\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"ZORG\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"B\"},\n\t\t\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tvar re render.Table\n\t\tre.SetViewSetting(&u.vs)\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tvar r model1.Row\n\t\t\tre.SetTable(u.ns, u.table)\n\n\t\t\tassert.Equal(t, u.eHeader, re.Header(u.ns))\n\t\t\trequire.NoError(t, re.Render(u.table.Rows[0], u.ns, &r))\n\t\t\tassert.Equal(t, u.eID, r.ID)\n\t\t\tassert.Equal(t, u.eFields, r.Fields)\n\t\t})\n\t}\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc makeNSGeneric() *metav1beta1.Table {\n\treturn &metav1beta1.Table{\n\t\tColumnDefinitions: []metav1beta1.TableColumnDefinition{\n\t\t\t{Name: \"NAMESPACE\"},\n\t\t\t{Name: \"a\"},\n\t\t\t{Name: \"b\"},\n\t\t\t{Name: \"c\"},\n\t\t},\n\t\tRows: []metav1beta1.TableRow{\n\t\t\t{\n\t\t\t\tObject: runtime.RawExtension{\n\t\t\t\t\tObject: &unstructured.Unstructured{\n\t\t\t\t\t\tObject: map[string]any{\n\t\t\t\t\t\t\t\"kind\":       \"fred\",\n\t\t\t\t\t\t\t\"apiVersion\": \"v1\",\n\t\t\t\t\t\t\t\"metadata\": map[string]any{\n\t\t\t\t\t\t\t\t\"namespace\": \"ns1\",\n\t\t\t\t\t\t\t\t\"name\":      \"fred\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCells: []any{\n\t\t\t\t\t\"ns1\",\n\t\t\t\t\t\"c1\",\n\t\t\t\t\t\"c2\",\n\t\t\t\t\t\"c3\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc makeNoNSGeneric() *metav1beta1.Table {\n\treturn &metav1beta1.Table{\n\t\tColumnDefinitions: []metav1beta1.TableColumnDefinition{\n\t\t\t{Name: \"a\"},\n\t\t\t{Name: \"b\"},\n\t\t\t{Name: \"c\"},\n\t\t},\n\t\tRows: []metav1beta1.TableRow{\n\t\t\t{\n\t\t\t\tObject: runtime.RawExtension{\n\t\t\t\t\tObject: &unstructured.Unstructured{\n\t\t\t\t\t\tObject: map[string]any{\n\t\t\t\t\t\t\t\"kind\":       \"fred\",\n\t\t\t\t\t\t\t\"apiVersion\": \"v1\",\n\t\t\t\t\t\t\t\"metadata\": map[string]any{\n\t\t\t\t\t\t\t\t\"name\": \"fred\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCells: []any{\n\t\t\t\t\t\"c1\",\n\t\t\t\t\t\"c2\",\n\t\t\t\t\t\"c3\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc makeAgeGeneric() *metav1beta1.Table {\n\treturn &metav1beta1.Table{\n\t\tColumnDefinitions: []metav1beta1.TableColumnDefinition{\n\t\t\t{Name: \"a\"},\n\t\t\t{Name: \"Age\"},\n\t\t\t{Name: \"c\"},\n\t\t},\n\t\tRows: []metav1beta1.TableRow{\n\t\t\t{\n\t\t\t\tObject: runtime.RawExtension{\n\t\t\t\t\tObject: &unstructured.Unstructured{\n\t\t\t\t\t\tObject: map[string]any{\n\t\t\t\t\t\t\t\"kind\":       \"fred\",\n\t\t\t\t\t\t\t\"apiVersion\": \"v1\",\n\t\t\t\t\t\t\t\"metadata\": map[string]any{\n\t\t\t\t\t\t\t\t\"name\": \"fred\",\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tCells: []any{\n\t\t\t\t\t\"c1\",\n\t\t\t\t\t\"2d\",\n\t\t\t\t\t\"c2\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/render/testdata/b1.txt",
    "content": "\nSummary:\n  Total:\t3.3544 secs\n  Slowest:\t0.1031 secs\n  Fastest:\t0.0310 secs\n  Average:\t0.0335 secs\n  Requests/sec:\t29.8116\n\n  Total data:\t61200 bytes\n  Size/request:\t612 bytes\n\nResponse time histogram:\n  0.031 [1]\t|\n  0.038 [92]\t|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n  0.045 [6]\t|■■■\n  0.053 [0]\t|\n  0.060 [0]\t|\n  0.067 [0]\t|\n  0.074 [0]\t|\n  0.081 [0]\t|\n  0.089 [0]\t|\n  0.096 [0]\t|\n  0.103 [1]\t|\n\n\nLatency distribution:\n  10% in 0.0314 secs\n  25% in 0.0317 secs\n  50% in 0.0320 secs\n  75% in 0.0327 secs\n  90% in 0.0369 secs\n  95% in 0.0394 secs\n  99% in 0.1031 secs\n\nDetails (average, fastest, slowest):\n  DNS+dialup:\t0.0001 secs, 0.0310 secs, 0.1031 secs\n  DNS-lookup:\t0.0000 secs, 0.0000 secs, 0.0049 secs\n  req write:\t0.0000 secs, 0.0000 secs, 0.0001 secs\n  resp wait:\t0.0330 secs, 0.0305 secs, 0.0973 secs\n  resp read:\t0.0005 secs, 0.0000 secs, 0.0039 secs\n\nStatus code distribution:\n  [200]\t100 responses"
  },
  {
    "path": "internal/render/testdata/b2.txt",
    "content": "Summary:\n  Total:\t3.3544 secs\n  Slowest:\t0.1031 secs\n  Fastest:\t0.0310 secs\n  Average:\t0.0335 secs\n  Requests/sec:\t29.8116\n\n  Total data:\t61200 bytes\n  Size/request:\t612 bytes\n\nResponse time histogram:\n  0.031 [1]\t|\n  0.038 [92]\t|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n  0.045 [6]\t|■■■\n  0.053 [0]\t|\n  0.060 [0]\t|\n  0.067 [0]\t|\n  0.074 [0]\t|\n  0.081 [0]\t|\n  0.089 [0]\t|\n  0.096 [0]\t|\n  0.103 [1]\t|\n\n\nLatency distribution:\n  10% in 0.0314 secs\n  25% in 0.0317 secs\n  50% in 0.0320 secs\n  75% in 0.0327 secs\n  90% in 0.0369 secs\n  95% in 0.0394 secs\n  99% in 0.1031 secs\n\nDetails (average, fastest, slowest):\n  DNS+dialup:\t0.0001 secs, 0.0310 secs, 0.1031 secs\n  DNS-lookup:\t0.0000 secs, 0.0000 secs, 0.0049 secs\n  req write:\t0.0000 secs, 0.0000 secs, 0.0001 secs\n  resp wait:\t0.0330 secs, 0.0305 secs, 0.0973 secs\n  resp read:\t0.0005 secs, 0.0000 secs, 0.0039 secs\n\nStatus code distribution:\n  [200]\t100 responses\n  [404] 2 responses\n  [500] 10 responses"
  },
  {
    "path": "internal/render/testdata/b3.txt",
    "content": "\nSummary:\n  Total:\t2.3688 secs\n  Slowest:\t0.0000 secs\n  Fastest:\t0.0000 secs\n  Average:\t NaN secs\n  Requests/sec:\t35.4606\n\n\nResponse time histogram:\n\n\nLatency distribution:\n\nDetails (average, fastest, slowest):\n  DNS+dialup:\t NaN secs, 0.0000 secs, 0.0000 secs\n  DNS-lookup:\t NaN secs, 0.0000 secs, 0.0000 secs\n  req write:\t NaN secs, 0.0000 secs, 0.0000 secs\n  resp wait:\t NaN secs, 0.0000 secs, 0.0000 secs\n  resp read:\t NaN secs, 0.0000 secs, 0.0000 secs\n\nStatus code distribution:\n\nError distribution:\n  [84]\tGet http://localhost:8081: dial tcp [::1]:8081: connect: connection refused"
  },
  {
    "path": "internal/render/testdata/b4.txt",
    "content": "\nSummary:\n  Total:\t3.3544 secs\n  Slowest:\t0.1031 secs\n  Fastest:\t0.0310 secs\n  Average:\t0.0335 secs\n  Requests/sec:\t29.8116\n\n  Total data:\t61200 bytes\n  Size/request:\t612 bytes\n\nResponse time histogram:\n  0.031 [1]\t|\n  0.038 [92]\t|■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■\n  0.045 [6]\t|■■■\n  0.053 [0]\t|\n  0.060 [0]\t|\n  0.067 [0]\t|\n  0.074 [0]\t|\n  0.081 [0]\t|\n  0.089 [0]\t|\n  0.096 [0]\t|\n  0.103 [1]\t|\n\n\nLatency distribution:\n  10% in 0.0314 secs\n  25% in 0.0317 secs\n  50% in 0.0320 secs\n  75% in 0.0327 secs\n  90% in 0.0369 secs\n  95% in 0.0394 secs\n  99% in 0.1031 secs\n\nDetails (average, fastest, slowest):\n  DNS+dialup:\t0.0001 secs, 0.0310 secs, 0.1031 secs\n  DNS-lookup:\t0.0000 secs, 0.0000 secs, 0.0049 secs\n  req write:\t0.0000 secs, 0.0000 secs, 0.0001 secs\n  resp wait:\t0.0330 secs, 0.0305 secs, 0.0973 secs\n  resp read:\t0.0005 secs, 0.0000 secs, 0.0039 secs\n\nStatus code distribution:\n  [200]\t100 responses\n  [204]\t50 responses\n  [202]\t10 responses"
  },
  {
    "path": "internal/render/testdata/cj.json",
    "content": "{\n  \"apiVersion\": \"batch/v1beta1\",\n  \"kind\": \"CronJob\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"batch/v1beta1\\\",\\\"kind\\\":\\\"CronJob\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"hello\\\",\\\"namespace\\\":\\\"default\\\"},\\\"spec\\\":{\\\"concurrencyPolicy\\\":\\\"Forbid\\\",\\\"jobTemplate\\\":{\\\"spec\\\":{\\\"template\\\":{\\\"spec\\\":{\\\"containers\\\":[{\\\"args\\\":[\\\"/bin/bash\\\",\\\"-c\\\",\\\"for i in {1..5}; do echo c1 $i; sleep 1; done\\\"],\\\"image\\\":\\\"blang/busybox-bash\\\",\\\"name\\\":\\\"c1\\\"}],\\\"restartPolicy\\\":\\\"OnFailure\\\"}}}},\\\"schedule\\\":\\\"*/1 * * * *\\\"}}\\n\"\n    },\n    \"creationTimestamp\": \"2019-08-30T15:19:01Z\",\n    \"name\": \"hello\",\n    \"namespace\": \"default\",\n    \"resourceVersion\": \"49753699\",\n    \"selfLink\": \"/apis/batch/v1beta1/namespaces/default/cronjobs/hello\",\n    \"uid\": \"7f0b856c-cb39-11e9-990f-42010a800218\"\n  },\n  \"spec\": {\n    \"concurrencyPolicy\": \"Forbid\",\n    \"failedJobsHistoryLimit\": 1,\n    \"jobTemplate\": {\n      \"metadata\": {\n        \"creationTimestamp\": null\n      },\n      \"spec\": {\n        \"template\": {\n          \"metadata\": {\n            \"creationTimestamp\": null\n          },\n          \"spec\": {\n            \"containers\": [\n              {\n                \"args\": [\n                  \"/bin/bash\",\n                  \"-c\",\n                  \"for i in {1..5}; do echo c1 $i; sleep 1; done\"\n                ],\n                \"image\": \"blang/busybox-bash\",\n                \"imagePullPolicy\": \"Always\",\n                \"name\": \"c1\",\n                \"resources\": {},\n                \"terminationMessagePath\": \"/dev/termination-log\",\n                \"terminationMessagePolicy\": \"File\"\n              }\n            ],\n            \"dnsPolicy\": \"ClusterFirst\",\n            \"restartPolicy\": \"OnFailure\",\n            \"schedulerName\": \"default-scheduler\",\n            \"securityContext\": {},\n            \"terminationGracePeriodSeconds\": 30\n          }\n        }\n      }\n    },\n    \"schedule\": \"*/1 * * * *\",\n    \"successfulJobsHistoryLimit\": 3,\n    \"suspend\": false\n  },\n  \"status\": {\n    \"lastScheduleTime\": \"2019-08-30T17:01:00Z\"\n  }\n}"
  },
  {
    "path": "internal/render/testdata/cm.json",
    "content": "{\n  \"apiVersion\": \"v1\",\n  \"data\": {\n    \"key1\": \"very\",\n    \"key2\": \"charm\"\n  },\n  \"kind\": \"ConfigMap\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"v1\\\",\\\"data\\\":{\\\"key1\\\":\\\"very\\\",\\\"key2\\\":\\\"charm\\\"},\\\"kind\\\":\\\"ConfigMap\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"blee\\\",\\\"namespace\\\":\\\"default\\\"}}\\n\"\n    },\n    \"creationTimestamp\": \"2019-06-05T21:56:55Z\",\n    \"name\": \"blee\",\n    \"namespace\": \"default\",\n    \"resourceVersion\": \"27009817\",\n    \"selfLink\": \"/api/v1/namespaces/default/configmaps/blee\",\n    \"uid\": \"d587a666-87dc-11e9-a8e8-42010a80015b\"\n  }\n}"
  },
  {
    "path": "internal/render/testdata/cr.json",
    "content": "{\n  \"apiVersion\": \"rbac.authorization.k8s.io/v1\",\n  \"kind\": \"ClusterRole\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"rbac.authorization.k8s.io/v1\\\",\\\"kind\\\":\\\"ClusterRole\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"blee\\\"},\\\"rules\\\":[{\\\"apiGroups\\\":[\\\"metrics.k8s.io\\\"],\\\"resources\\\":[\\\"nodes\\\"],\\\"verbs\\\":[\\\"list\\\",\\\"watch\\\"]},{\\\"apiGroups\\\":[\\\"\\\"],\\\"resources\\\":[\\\"nodes\\\",\\\"configmaps\\\"],\\\"verbs\\\":[\\\"list\\\"]},{\\\"apiGroups\\\":[\\\"\\\"],\\\"resourceNames\\\":[\\\"kube-system\\\"],\\\"resources\\\":[\\\"namespaces\\\"],\\\"verbs\\\":[\\\"get\\\",\\\"watch\\\"]},{\\\"apiGroups\\\":[\\\"\\\"],\\\"resources\\\":[\\\"pods\\\"],\\\"verbs\\\":[\\\"get\\\",\\\"list\\\",\\\"watch\\\",\\\"delete\\\"]}]}\\n\"\n    },\n    \"creationTimestamp\": \"2019-06-04T16:48:34Z\",\n    \"name\": \"blee\",\n    \"resourceVersion\": \"26708289\",\n    \"selfLink\": \"/apis/rbac.authorization.k8s.io/v1/clusterroles/blee\",\n    \"uid\": \"97dbe984-86e8-11e9-a8e8-42010a80015b\"\n  },\n  \"rules\": [\n    {\n      \"apiGroups\": [\n        \"metrics.k8s.io\"\n      ],\n      \"resources\": [\n        \"nodes\"\n      ],\n      \"verbs\": [\n        \"list\",\n        \"watch\"\n      ]\n    },\n    {\n      \"apiGroups\": [\n        \"\"\n      ],\n      \"resources\": [\n        \"nodes\",\n        \"configmaps\"\n      ],\n      \"verbs\": [\n        \"list\"\n      ]\n    },\n    {\n      \"apiGroups\": [\n        \"\"\n      ],\n      \"resourceNames\": [\n        \"kube-system\"\n      ],\n      \"resources\": [\n        \"namespaces\"\n      ],\n      \"verbs\": [\n        \"get\",\n        \"watch\"\n      ]\n    },\n    {\n      \"apiGroups\": [\n        \"\"\n      ],\n      \"resources\": [\n        \"pods\"\n      ],\n      \"verbs\": [\n        \"get\",\n        \"list\",\n        \"watch\",\n        \"delete\"\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "internal/render/testdata/crb.json",
    "content": "{\n  \"apiVersion\": \"rbac.authorization.k8s.io/v1\",\n  \"kind\": \"ClusterRoleBinding\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"rbac.authorization.k8s.io/v1\\\",\\\"kind\\\":\\\"ClusterRoleBinding\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"blee\\\"},\\\"roleRef\\\":{\\\"apiGroup\\\":\\\"rbac.authorization.k8s.io\\\",\\\"kind\\\":\\\"ClusterRole\\\",\\\"name\\\":\\\"blee\\\"},\\\"subjects\\\":[{\\\"apiGroup\\\":\\\"rbac.authorization.k8s.io\\\",\\\"kind\\\":\\\"User\\\",\\\"name\\\":\\\"fernand\\\"}]}\\n\"\n    },\n    \"creationTimestamp\": \"2019-06-04T16:48:35Z\",\n    \"name\": \"blee\",\n    \"resourceVersion\": \"26689100\",\n    \"selfLink\": \"/apis/rbac.authorization.k8s.io/v1/clusterrolebindings/blee\",\n    \"uid\": \"97e5f84d-86e8-11e9-a8e8-42010a80015b\"\n  },\n  \"roleRef\": {\n    \"apiGroup\": \"rbac.authorization.k8s.io\",\n    \"kind\": \"ClusterRole\",\n    \"name\": \"blee\"\n  },\n  \"subjects\": [\n    {\n      \"apiGroup\": \"rbac.authorization.k8s.io\",\n      \"kind\": \"User\",\n      \"name\": \"fernand\"\n    }\n  ]\n}"
  },
  {
    "path": "internal/render/testdata/crd.json",
    "content": "{\n  \"apiVersion\": \"apiextensions.k8s.io/v1\",\n  \"kind\": \"CustomResourceDefinition\",\n  \"metadata\": {\n    \"annotations\": {\n      \"helm.sh/hook\": \"crd-install\",\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"apiextensions.k8s.io/v1\\\",\\\"kind\\\":\\\"CustomResourceDefinition\\\",\\\"metadata\\\":{\\\"annotations\\\":{\\\"helm.sh/hook\\\":\\\"crd-install\\\"},\\\"labels\\\":{\\\"addonmanager.kubernetes.io/mode\\\":\\\"Reconcile\\\",\\\"app\\\":\\\"mixer\\\",\\\"istio\\\":\\\"mixer-adapter\\\",\\\"k8s-app\\\":\\\"istio\\\",\\\"package\\\":\\\"adapter\\\"},\\\"name\\\":\\\"adapters.config.istio.io\\\",\\\"namespace\\\":\\\"\\\"},\\\"spec\\\":{\\\"group\\\":\\\"config.istio.io\\\",\\\"names\\\":{\\\"categories\\\":[\\\"istio-io\\\",\\\"policy-istio-io\\\"],\\\"kind\\\":\\\"adapter\\\",\\\"plural\\\":\\\"adapters\\\",\\\"singular\\\":\\\"adapter\\\"},\\\"scope\\\":\\\"Namespaced\\\",\\\"version\\\":\\\"v1alpha2\\\"}}\\n\"\n    },\n    \"creationTimestamp\": \"2019-02-05T22:04:29Z\",\n    \"generation\": 1,\n    \"labels\": {\n      \"addonmanager.kubernetes.io/mode\": \"Reconcile\",\n      \"app\": \"mixer\",\n      \"istio\": \"mixer-adapter\",\n      \"k8s-app\": \"istio\",\n      \"package\": \"adapter\"\n    },\n    \"name\": \"adapters.config.istio.io\",\n    \"resourceVersion\": \"37115599\",\n    \"selfLink\": \"/apis/apiextensions.k8s.io/v1/customresourcedefinitions/adapters.config.istio.io\",\n    \"uid\": \"029b8c3e-2992-11e9-81cd-42010a80005b\"\n  },\n  \"spec\": {\n    \"additionalPrinterColumns\": [\n      {\n        \"JSONPath\": \".metadata.creationTimestamp\",\n        \"description\": \"CreationTimestamp is a timestamp representing the server time when this object was created. It is not guaranteed to be set in happens-before order across separate operations. Clients may not set this value. It is represented in RFC3339 form and is in UTC.\\n\\nPopulated by the system. Read-only. Null for lists. More info: https://git.k8s.io/community/contributors/devel/api-conventions.md#metadata\",\n        \"name\": \"Age\",\n        \"type\": \"date\"\n      }\n    ],\n    \"group\": \"config.istio.io\",\n    \"names\": {\n      \"categories\": [\n        \"istio-io\",\n        \"policy-istio-io\"\n      ],\n      \"kind\": \"adapter\",\n      \"listKind\": \"adapterList\",\n      \"plural\": \"adapters\",\n      \"singular\": \"adapter\"\n    },\n    \"scope\": \"Namespaced\",\n    \"version\": \"v1alpha2\",\n    \"versions\": [\n      {\n        \"name\": \"v1alpha2\",\n        \"served\": true,\n        \"storage\": true\n      }\n    ]\n  },\n  \"status\": {\n    \"acceptedNames\": {\n      \"categories\": [\n        \"istio-io\",\n        \"policy-istio-io\"\n      ],\n      \"kind\": \"adapter\",\n      \"listKind\": \"adapterList\",\n      \"plural\": \"adapters\",\n      \"singular\": \"adapter\"\n    },\n    \"conditions\": [\n      {\n        \"lastTransitionTime\": \"2019-02-05T22:04:29Z\",\n        \"message\": \"no conflicts found\",\n        \"reason\": \"NoConflicts\",\n        \"status\": \"True\",\n        \"type\": \"NamesAccepted\"\n      },\n      {\n        \"lastTransitionTime\": null,\n        \"message\": \"the initial names have been accepted\",\n        \"reason\": \"InitialNamesAccepted\",\n        \"status\": \"True\",\n        \"type\": \"Established\"\n      }\n    ],\n    \"storedVersions\": [\n      \"v1alpha2\"\n    ]\n  }\n}"
  },
  {
    "path": "internal/render/testdata/dp.json",
    "content": "{\n  \"apiVersion\": \"apps/v1\",\n  \"kind\": \"Deployment\",\n  \"metadata\": {\n    \"annotations\": {\n      \"deployment.kubernetes.io/revision\": \"1\",\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"apps/v1beta1\\\",\\\"kind\\\":\\\"Deployment\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"labels\\\":{\\\"app\\\":\\\"icx-db\\\"},\\\"name\\\":\\\"icx-db\\\",\\\"namespace\\\":\\\"icx\\\"},\\\"spec\\\":{\\\"replicas\\\":1,\\\"selector\\\":{\\\"matchLabels\\\":{\\\"app\\\":\\\"icx-db\\\"}},\\\"template\\\":{\\\"metadata\\\":{\\\"labels\\\":{\\\"app\\\":\\\"icx-db\\\"}},\\\"spec\\\":{\\\"containers\\\":[{\\\"env\\\":[{\\\"name\\\":\\\"POSTGRES_USER\\\",\\\"valueFrom\\\":{\\\"secretKeyRef\\\":{\\\"key\\\":\\\"pg_user\\\",\\\"name\\\":\\\"icx-creds\\\"}}},{\\\"name\\\":\\\"POSTGRES_PASSWORD\\\",\\\"valueFrom\\\":{\\\"secretKeyRef\\\":{\\\"key\\\":\\\"pg_pwd\\\",\\\"name\\\":\\\"icx-creds\\\"}}}],\\\"image\\\":\\\"postgres:9.2-alpine\\\",\\\"imagePullPolicy\\\":\\\"IfNotPresent\\\",\\\"name\\\":\\\"icx-db\\\",\\\"ports\\\":[{\\\"containerPort\\\":5432,\\\"name\\\":\\\"client\\\"}],\\\"resources\\\":{\\\"limits\\\":{\\\"cpu\\\":\\\"250m\\\",\\\"memory\\\":\\\"512Mi\\\"},\\\"requests\\\":{\\\"cpu\\\":\\\"250m\\\",\\\"memory\\\":\\\"256Mi\\\"}}}]}}}}\\n\"\n    },\n    \"creationTimestamp\": \"2019-07-14T04:54:17Z\",\n    \"generation\": 1,\n    \"labels\": {\n      \"app\": \"icx-db\"\n    },\n    \"name\": \"icx-db\",\n    \"namespace\": \"icx\",\n    \"resourceVersion\": \"37116271\",\n    \"selfLink\": \"/apis/apps/v1/namespaces/icx/deployments/icx-db\",\n    \"uid\": \"6f6143bc-a5f3-11e9-990f-42010a800218\"\n  },\n  \"spec\": {\n    \"progressDeadlineSeconds\": 600,\n    \"replicas\": 1,\n    \"revisionHistoryLimit\": 2,\n    \"selector\": {\n      \"matchLabels\": {\n        \"app\": \"icx-db\"\n      }\n    },\n    \"strategy\": {\n      \"rollingUpdate\": {\n        \"maxSurge\": \"25%\",\n        \"maxUnavailable\": \"25%\"\n      },\n      \"type\": \"RollingUpdate\"\n    },\n    \"template\": {\n      \"metadata\": {\n        \"creationTimestamp\": null,\n        \"labels\": {\n          \"app\": \"icx-db\"\n        }\n      },\n      \"spec\": {\n        \"containers\": [\n          {\n            \"env\": [\n              {\n                \"name\": \"POSTGRES_USER\",\n                \"valueFrom\": {\n                  \"secretKeyRef\": {\n                    \"key\": \"pg_user\",\n                    \"name\": \"icx-creds\"\n                  }\n                }\n              },\n              {\n                \"name\": \"POSTGRES_PASSWORD\",\n                \"valueFrom\": {\n                  \"secretKeyRef\": {\n                    \"key\": \"pg_pwd\",\n                    \"name\": \"icx-creds\"\n                  }\n                }\n              }\n            ],\n            \"image\": \"postgres:9.2-alpine\",\n            \"imagePullPolicy\": \"IfNotPresent\",\n            \"name\": \"icx-db\",\n            \"ports\": [\n              {\n                \"containerPort\": 5432,\n                \"name\": \"client\",\n                \"protocol\": \"TCP\"\n              }\n            ],\n            \"resources\": {\n              \"limits\": {\n                \"cpu\": \"250m\",\n                \"memory\": \"512Mi\"\n              },\n              \"requests\": {\n                \"cpu\": \"250m\",\n                \"memory\": \"256Mi\"\n              }\n            },\n            \"terminationMessagePath\": \"/dev/termination-log\",\n            \"terminationMessagePolicy\": \"File\"\n          }\n        ],\n        \"dnsPolicy\": \"ClusterFirst\",\n        \"restartPolicy\": \"Always\",\n        \"schedulerName\": \"default-scheduler\",\n        \"securityContext\": {},\n        \"terminationGracePeriodSeconds\": 30\n      }\n    }\n  },\n  \"status\": {\n    \"availableReplicas\": 1,\n    \"conditions\": [\n      {\n        \"lastTransitionTime\": \"2019-07-14T04:54:20Z\",\n        \"lastUpdateTime\": \"2019-07-14T04:54:20Z\",\n        \"message\": \"Deployment has minimum availability.\",\n        \"reason\": \"MinimumReplicasAvailable\",\n        \"status\": \"True\",\n        \"type\": \"Available\"\n      },\n      {\n        \"lastTransitionTime\": \"2019-07-14T04:54:17Z\",\n        \"lastUpdateTime\": \"2019-07-14T04:54:20Z\",\n        \"message\": \"ReplicaSet \\\"icx-db-7d4b578979\\\" has successfully progressed.\",\n        \"reason\": \"NewReplicaSetAvailable\",\n        \"status\": \"True\",\n        \"type\": \"Progressing\"\n      }\n    ],\n    \"observedGeneration\": 1,\n    \"readyReplicas\": 1,\n    \"replicas\": 1,\n    \"updatedReplicas\": 1\n  }\n}"
  },
  {
    "path": "internal/render/testdata/ds.json",
    "content": "{\n  \"apiVersion\": \"apps/v1\",\n  \"kind\": \"DaemonSet\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"apps/v1\\\",\\\"kind\\\":\\\"DaemonSet\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"labels\\\":{\\\"addonmanager.kubernetes.io/mode\\\":\\\"Reconcile\\\",\\\"k8s-app\\\":\\\"fluentd-gcp\\\",\\\"kubernetes.io/cluster-service\\\":\\\"true\\\",\\\"version\\\":\\\"v3.2.0\\\"},\\\"name\\\":\\\"fluentd-gcp-v3.2.0\\\",\\\"namespace\\\":\\\"kube-system\\\"},\\\"spec\\\":{\\\"template\\\":{\\\"metadata\\\":{\\\"annotations\\\":{\\\"scheduler.alpha.kubernetes.io/critical-pod\\\":\\\"\\\"},\\\"labels\\\":{\\\"k8s-app\\\":\\\"fluentd-gcp\\\",\\\"kubernetes.io/cluster-service\\\":\\\"true\\\",\\\"version\\\":\\\"v3.2.0\\\"}},\\\"spec\\\":{\\\"containers\\\":[{\\\"env\\\":[{\\\"name\\\":\\\"NODE_NAME\\\",\\\"valueFrom\\\":{\\\"fieldRef\\\":{\\\"apiVersion\\\":\\\"v1\\\",\\\"fieldPath\\\":\\\"spec.nodeName\\\"}}},{\\\"name\\\":\\\"STACKDRIVER_METADATA_AGENT_URL\\\",\\\"value\\\":\\\"http://$(NODE_NAME):8799\\\"}],\\\"image\\\":\\\"gcr.io/stackdriver-agents/stackdriver-logging-agent:0.6-1.6.0-1\\\",\\\"livenessProbe\\\":{\\\"exec\\\":{\\\"command\\\":[\\\"/bin/sh\\\",\\\"-c\\\",\\\"LIVENESS_THRESHOLD_SECONDS=${LIVENESS_THRESHOLD_SECONDS:-300}; STUCK_THRESHOLD_SECONDS=${LIVENESS_THRESHOLD_SECONDS:-900}; if [ ! -e /var/log/fluentd-buffers ]; then\\\\n  exit 1;\\\\nfi; touch -d \\\\\\\"${STUCK_THRESHOLD_SECONDS} seconds ago\\\\\\\" /tmp/marker-stuck; if [[ -z \\\\\\\"$(find /var/log/fluentd-buffers -type f -newer /tmp/marker-stuck -print -quit)\\\\\\\" ]]; then\\\\n  rm -rf /var/log/fluentd-buffers;\\\\n  exit 1;\\\\nfi; touch -d \\\\\\\"${LIVENESS_THRESHOLD_SECONDS} seconds ago\\\\\\\" /tmp/marker-liveness; if [[ -z \\\\\\\"$(find /var/log/fluentd-buffers -type f -newer /tmp/marker-liveness -print -quit)\\\\\\\" ]]; then\\\\n  exit 1;\\\\nfi;\\\\n\\\"]},\\\"initialDelaySeconds\\\":600,\\\"periodSeconds\\\":60},\\\"name\\\":\\\"fluentd-gcp\\\",\\\"volumeMounts\\\":[{\\\"mountPath\\\":\\\"/var/log\\\",\\\"name\\\":\\\"varlog\\\"},{\\\"mountPath\\\":\\\"/var/lib/docker/containers\\\",\\\"name\\\":\\\"varlibdockercontainers\\\",\\\"readOnly\\\":true},{\\\"mountPath\\\":\\\"/etc/google-fluentd/config.d\\\",\\\"name\\\":\\\"config-volume\\\"}]},{\\\"command\\\":[\\\"/monitor\\\",\\\"--stackdriver-prefix=container.googleapis.com/internal/addons\\\",\\\"--api-override=https://monitoring.googleapis.com/\\\",\\\"--source=fluentd:http://localhost:24231?whitelisted=stackdriver_successful_requests_count,stackdriver_failed_requests_count,stackdriver_ingested_entries_count,stackdriver_dropped_entries_count\\\",\\\"--pod-id=$(POD_NAME)\\\",\\\"--namespace-id=$(POD_NAMESPACE)\\\"],\\\"env\\\":[{\\\"name\\\":\\\"POD_NAME\\\",\\\"valueFrom\\\":{\\\"fieldRef\\\":{\\\"fieldPath\\\":\\\"metadata.name\\\"}}},{\\\"name\\\":\\\"POD_NAMESPACE\\\",\\\"valueFrom\\\":{\\\"fieldRef\\\":{\\\"fieldPath\\\":\\\"metadata.namespace\\\"}}}],\\\"image\\\":\\\"k8s.gcr.io/prometheus-to-sd:v0.3.1\\\",\\\"name\\\":\\\"prometheus-to-sd-exporter\\\"}],\\\"dnsPolicy\\\":\\\"Default\\\",\\\"hostNetwork\\\":true,\\\"nodeSelector\\\":{\\\"beta.kubernetes.io/fluentd-ds-ready\\\":\\\"true\\\"},\\\"priorityClassName\\\":\\\"system-node-critical\\\",\\\"serviceAccountName\\\":\\\"fluentd-gcp\\\",\\\"terminationGracePeriodSeconds\\\":60,\\\"tolerations\\\":[{\\\"effect\\\":\\\"NoExecute\\\",\\\"operator\\\":\\\"Exists\\\"},{\\\"effect\\\":\\\"NoSchedule\\\",\\\"operator\\\":\\\"Exists\\\"}],\\\"volumes\\\":[{\\\"hostPath\\\":{\\\"path\\\":\\\"/var/log\\\"},\\\"name\\\":\\\"varlog\\\"},{\\\"hostPath\\\":{\\\"path\\\":\\\"/var/lib/docker/containers\\\"},\\\"name\\\":\\\"varlibdockercontainers\\\"},{\\\"configMap\\\":{\\\"name\\\":\\\"fluentd-gcp-config-old-v1.2.5\\\"},\\\"name\\\":\\\"config-volume\\\"}]}},\\\"updateStrategy\\\":{\\\"type\\\":\\\"RollingUpdate\\\"}}}\\n\"\n    },\n    \"creationTimestamp\": \"2019-04-12T23:35:36Z\",\n    \"generation\": 2,\n    \"labels\": {\n      \"addonmanager.kubernetes.io/mode\": \"Reconcile\",\n      \"k8s-app\": \"fluentd-gcp\",\n      \"kubernetes.io/cluster-service\": \"true\",\n      \"version\": \"v3.2.0\"\n    },\n    \"name\": \"fluentd-gcp-v3.2.0\",\n    \"namespace\": \"kube-system\",\n    \"resourceVersion\": \"34805583\",\n    \"selfLink\": \"/apis/apps/v1/namespaces/kube-system/daemonsets/fluentd-gcp-v3.2.0\",\n    \"uid\": \"ac95611f-5d7b-11e9-af05-42010a800018\"\n  },\n  \"spec\": {\n    \"revisionHistoryLimit\": 10,\n    \"selector\": {\n      \"matchLabels\": {\n        \"k8s-app\": \"fluentd-gcp\",\n        \"kubernetes.io/cluster-service\": \"true\",\n        \"version\": \"v3.2.0\"\n      }\n    },\n    \"template\": {\n      \"metadata\": {\n        \"annotations\": {\n          \"scheduler.alpha.kubernetes.io/critical-pod\": \"\"\n        },\n        \"creationTimestamp\": null,\n        \"labels\": {\n          \"k8s-app\": \"fluentd-gcp\",\n          \"kubernetes.io/cluster-service\": \"true\",\n          \"version\": \"v3.2.0\"\n        }\n      },\n      \"spec\": {\n        \"containers\": [\n          {\n            \"env\": [\n              {\n                \"name\": \"NODE_NAME\",\n                \"valueFrom\": {\n                  \"fieldRef\": {\n                    \"apiVersion\": \"v1\",\n                    \"fieldPath\": \"spec.nodeName\"\n                  }\n                }\n              },\n              {\n                \"name\": \"STACKDRIVER_METADATA_AGENT_URL\",\n                \"value\": \"http://$(NODE_NAME):8799\"\n              }\n            ],\n            \"image\": \"gcr.io/stackdriver-agents/stackdriver-logging-agent:0.6-1.6.0-1\",\n            \"imagePullPolicy\": \"IfNotPresent\",\n            \"livenessProbe\": {\n              \"exec\": {\n                \"command\": [\n                  \"/bin/sh\",\n                  \"-c\",\n                  \"LIVENESS_THRESHOLD_SECONDS=${LIVENESS_THRESHOLD_SECONDS:-300}; STUCK_THRESHOLD_SECONDS=${LIVENESS_THRESHOLD_SECONDS:-900}; if [ ! -e /var/log/fluentd-buffers ]; then\\n  exit 1;\\nfi; touch -d \\\"${STUCK_THRESHOLD_SECONDS} seconds ago\\\" /tmp/marker-stuck; if [[ -z \\\"$(find /var/log/fluentd-buffers -type f -newer /tmp/marker-stuck -print -quit)\\\" ]]; then\\n  rm -rf /var/log/fluentd-buffers;\\n  exit 1;\\nfi; touch -d \\\"${LIVENESS_THRESHOLD_SECONDS} seconds ago\\\" /tmp/marker-liveness; if [[ -z \\\"$(find /var/log/fluentd-buffers -type f -newer /tmp/marker-liveness -print -quit)\\\" ]]; then\\n  exit 1;\\nfi;\\n\"\n                ]\n              },\n              \"failureThreshold\": 3,\n              \"initialDelaySeconds\": 600,\n              \"periodSeconds\": 60,\n              \"successThreshold\": 1,\n              \"timeoutSeconds\": 1\n            },\n            \"name\": \"fluentd-gcp\",\n            \"resources\": {\n              \"limits\": {\n                \"cpu\": \"1\",\n                \"memory\": \"500Mi\"\n              },\n              \"requests\": {\n                \"cpu\": \"100m\",\n                \"memory\": \"200Mi\"\n              }\n            },\n            \"terminationMessagePath\": \"/dev/termination-log\",\n            \"terminationMessagePolicy\": \"File\",\n            \"volumeMounts\": [\n              {\n                \"mountPath\": \"/var/log\",\n                \"name\": \"varlog\"\n              },\n              {\n                \"mountPath\": \"/var/lib/docker/containers\",\n                \"name\": \"varlibdockercontainers\",\n                \"readOnly\": true\n              },\n              {\n                \"mountPath\": \"/etc/google-fluentd/config.d\",\n                \"name\": \"config-volume\"\n              }\n            ]\n          },\n          {\n            \"command\": [\n              \"/monitor\",\n              \"--stackdriver-prefix=container.googleapis.com/internal/addons\",\n              \"--api-override=https://monitoring.googleapis.com/\",\n              \"--source=fluentd:http://localhost:24231?whitelisted=stackdriver_successful_requests_count,stackdriver_failed_requests_count,stackdriver_ingested_entries_count,stackdriver_dropped_entries_count\",\n              \"--pod-id=$(POD_NAME)\",\n              \"--namespace-id=$(POD_NAMESPACE)\"\n            ],\n            \"env\": [\n              {\n                \"name\": \"POD_NAME\",\n                \"valueFrom\": {\n                  \"fieldRef\": {\n                    \"apiVersion\": \"v1\",\n                    \"fieldPath\": \"metadata.name\"\n                  }\n                }\n              },\n              {\n                \"name\": \"POD_NAMESPACE\",\n                \"valueFrom\": {\n                  \"fieldRef\": {\n                    \"apiVersion\": \"v1\",\n                    \"fieldPath\": \"metadata.namespace\"\n                  }\n                }\n              }\n            ],\n            \"image\": \"k8s.gcr.io/prometheus-to-sd:v0.3.1\",\n            \"imagePullPolicy\": \"IfNotPresent\",\n            \"name\": \"prometheus-to-sd-exporter\",\n            \"resources\": {},\n            \"terminationMessagePath\": \"/dev/termination-log\",\n            \"terminationMessagePolicy\": \"File\"\n          }\n        ],\n        \"dnsPolicy\": \"Default\",\n        \"hostNetwork\": true,\n        \"nodeSelector\": {\n          \"beta.kubernetes.io/fluentd-ds-ready\": \"true\"\n        },\n        \"priorityClassName\": \"system-node-critical\",\n        \"restartPolicy\": \"Always\",\n        \"schedulerName\": \"default-scheduler\",\n        \"securityContext\": {},\n        \"serviceAccount\": \"fluentd-gcp\",\n        \"serviceAccountName\": \"fluentd-gcp\",\n        \"terminationGracePeriodSeconds\": 60,\n        \"tolerations\": [\n          {\n            \"effect\": \"NoExecute\",\n            \"operator\": \"Exists\"\n          },\n          {\n            \"effect\": \"NoSchedule\",\n            \"operator\": \"Exists\"\n          }\n        ],\n        \"volumes\": [\n          {\n            \"hostPath\": {\n              \"path\": \"/var/log\",\n              \"type\": \"\"\n            },\n            \"name\": \"varlog\"\n          },\n          {\n            \"hostPath\": {\n              \"path\": \"/var/lib/docker/containers\",\n              \"type\": \"\"\n            },\n            \"name\": \"varlibdockercontainers\"\n          },\n          {\n            \"configMap\": {\n              \"defaultMode\": 420,\n              \"name\": \"fluentd-gcp-config-old-v1.2.5\"\n            },\n            \"name\": \"config-volume\"\n          }\n        ]\n      }\n    },\n    \"templateGeneration\": 2,\n    \"updateStrategy\": {\n      \"rollingUpdate\": {\n        \"maxUnavailable\": 1\n      },\n      \"type\": \"RollingUpdate\"\n    }\n  },\n  \"status\": {\n    \"currentNumberScheduled\": 2,\n    \"desiredNumberScheduled\": 2,\n    \"numberAvailable\": 2,\n    \"numberMisscheduled\": 0,\n    \"numberReady\": 2,\n    \"observedGeneration\": 2,\n    \"updatedNumberScheduled\": 2\n  }\n}"
  },
  {
    "path": "internal/render/testdata/ep.json",
    "content": "{\n  \"apiVersion\": \"v1\",\n  \"kind\": \"Endpoints\",\n  \"metadata\": {\n    \"creationTimestamp\": \"2019-07-10T23:10:43Z\",\n    \"name\": \"blee\",\n    \"namespace\": \"ns-1\"\n  },\n  \"subsets\": [\n    {\n        \"addresses\": [\n            {\n                \"ip\": \"10.0.0.67\",\n                \"nodeName\": \"n-1\",\n                \"targetRef\": {\n                    \"kind\": \"Pod\",\n                    \"name\": \"blah\",\n                    \"namespace\": \"blee\"\n                }\n            }\n        ],\n        \"ports\": [\n            {\n                \"name\": \"http\",\n                \"port\": 8080,\n                \"protocol\": \"TCP\"\n            }\n        ]\n    }\n]\n}"
  },
  {
    "path": "internal/render/testdata/eps.json",
    "content": "{\n  \"apiVersion\": \"discovery.k8s.io/v1\",\n  \"kind\": \"EndpointSlice\",\n  \"metadata\": {\n      \"creationTimestamp\": \"2025-04-17T22:14:13Z\",\n      \"name\": \"fred\",\n      \"namespace\": \"blee\"\n  },\n  \"addressType\": \"IPv4\",\n  \"endpoints\": [\n      {\n          \"addresses\": [\n              \"172.20.0.2\"\n          ],\n          \"conditions\": {\n              \"ready\": true,\n              \"serving\": true,\n              \"terminating\": false\n          },\n          \"nodeName\": \"n-1\",\n          \"targetRef\": {\n              \"kind\": \"Pod\",\n              \"name\": \"zorg\",\n              \"namespace\": \"kube-system\"\n          }\n      },\n      {\n          \"addresses\": [\n              \"172.20.0.3\"\n          ],\n          \"conditions\": {\n              \"ready\": true,\n              \"serving\": true,\n              \"terminating\": false\n          },\n          \"nodeName\": \"n-1\",\n          \"targetRef\": {\n              \"kind\": \"Pod\",\n              \"name\": \"zorg\",\n              \"namespace\": \"kube-system\"\n          }\n      }\n  ],\n  \"ports\": [\n      {\n          \"name\": \"peer-service\",\n          \"port\": 4244,\n          \"protocol\": \"TCP\"\n      }\n  ]\n}"
  },
  {
    "path": "internal/render/testdata/ev.json",
    "content": "{\n  \"apiVersion\": \"v1\",\n  \"count\": 1,\n  \"eventTime\": null,\n  \"firstTimestamp\": \"2019-08-30T20:43:05Z\",\n  \"involvedObject\": {\n    \"apiVersion\": \"v1\",\n    \"fieldPath\": \"spec.containers{c1}\",\n    \"kind\": \"Pod\",\n    \"name\": \"hello-1567197780-mn4mv\",\n    \"namespace\": \"default\",\n    \"resourceVersion\": \"49798867\",\n    \"uid\": \"c31fdeb8-cb66-11e9-990f-42010a800218\"\n  },\n  \"kind\": \"Event\",\n  \"lastTimestamp\": \"2019-08-30T20:43:05Z\",\n  \"message\": \"Successfully pulled image \\\"blang/busybox-bash\\\"\",\n  \"metadata\": {\n    \"creationTimestamp\": \"2019-08-30T20:43:05Z\",\n    \"name\": \"hello-1567197780-mn4mv.15bfce150bd764dd\",\n    \"namespace\": \"default\",\n    \"resourceVersion\": \"590733\",\n    \"selfLink\": \"/api/v1/namespaces/default/events/hello-1567197780-mn4mv.15bfce150bd764dd\",\n    \"uid\": \"c443d4b3-cb66-11e9-990f-42010a800218\"\n  },\n  \"reason\": \"Pulled\",\n  \"reportingComponent\": \"\",\n  \"reportingInstance\": \"\",\n  \"source\": {\n    \"component\": \"kubelet\",\n    \"host\": \"gke-k9s-default-pool-0fa2fb89-qnkc\"\n  },\n  \"type\": \"Normal\"\n}"
  },
  {
    "path": "internal/render/testdata/hpa.json",
    "content": "{\n  \"apiVersion\": \"autoscaling/v1\",\n  \"kind\": \"HorizontalPodAutoscaler\",\n  \"metadata\": {\n    \"annotations\": {\n      \"autoscaling.alpha.kubernetes.io/conditions\": \"[{\\\"type\\\":\\\"AbleToScale\\\",\\\"status\\\":\\\"False\\\",\\\"lastTransitionTime\\\":\\\"2019-07-19T20:56:05Z\\\",\\\"reason\\\":\\\"FailedGetScale\\\",\\\"message\\\":\\\"the HPA controller was unable to get the target's current scale: deployments/scale.extensions \\\\\\\"nginx\\\\\\\" not found\\\"}]\",\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"autoscaling/v1\\\",\\\"kind\\\":\\\"HorizontalPodAutoscaler\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"nginx\\\",\\\"namespace\\\":\\\"default\\\"},\\\"spec\\\":{\\\"maxReplicas\\\":10,\\\"minReplicas\\\":1,\\\"scaleTargetRef\\\":{\\\"apiVersion\\\":\\\"apps/v1\\\",\\\"kind\\\":\\\"Deployment\\\",\\\"name\\\":\\\"nginx\\\"},\\\"targetCPUUtilizationPercentage\\\":10}}\\n\"\n    },\n    \"creationTimestamp\": \"2019-07-19T20:55:50Z\",\n    \"name\": \"nginx\",\n    \"namespace\": \"default\",\n    \"resourceVersion\": \"38623948\",\n    \"selfLink\": \"/apis/autoscaling/v1/namespaces/default/horizontalpodautoscalers/nginx\",\n    \"uid\": \"97104229-aa67-11e9-990f-42010a800218\"\n  },\n  \"spec\": {\n    \"maxReplicas\": 10,\n    \"minReplicas\": 1,\n    \"scaleTargetRef\": {\n      \"apiVersion\": \"apps/v1\",\n      \"kind\": \"Deployment\",\n      \"name\": \"nginx\"\n    },\n    \"targetCPUUtilizationPercentage\": 10\n  },\n  \"status\": {\n    \"currentReplicas\": 0,\n    \"desiredReplicas\": 0\n  }\n}"
  },
  {
    "path": "internal/render/testdata/ing.json",
    "content": "{\n  \"apiVersion\": \"networking.k8s.io/v1\",\n  \"kind\": \"Ingress\",\n  \"metadata\": {\n    \"labels\": {\n      \"role\": \"ingress\"\n    },\n    \"annotations\": {\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"networking.k8s.io/v1\\\",\\\"kind\\\":\\\"Ingress\\\",\\\"metadata\\\":{\\\"annotations\\\":{\\\"nginx.ingress.kubernetes.io/rewrite-target\\\":\\\"/\\\"},\\\"name\\\":\\\"test-ingress\\\",\\\"namespace\\\":\\\"default\\\"},\\\"spec\\\":{\\\"rules\\\":[{\\\"http\\\":{\\\"paths\\\":[{\\\"backend\\\":{\\\"serviceName\\\":\\\"test\\\",\\\"servicePort\\\":80},\\\"path\\\":\\\"/testpath\\\"}]}}]}}\\n\",\n      \"nginx.ingress.kubernetes.io/rewrite-target\": \"/\"\n    },\n    \"creationTimestamp\": \"2019-08-30T20:53:52Z\",\n    \"generation\": 1,\n    \"name\": \"test-ingress\",\n    \"namespace\": \"default\",\n    \"resourceVersion\": \"49801063\",\n    \"selfLink\": \"/apis/networking.k8s.io/v1/namespaces/default/ingresses/test-ingress\",\n    \"uid\": \"45e44c1d-cb68-11e9-990f-42010a800218\"\n  },\n  \"spec\": {\n    \"rules\": [\n      {\n        \"http\": {\n          \"paths\": [\n            {\n              \"backend\": {\n                \"serviceName\": \"test\",\n                \"servicePort\": 80\n              },\n              \"path\": \"/testpath\"\n            }\n          ]\n        }\n      }\n    ]\n  },\n  \"status\": {\n    \"loadBalancer\": {}\n  }\n}"
  },
  {
    "path": "internal/render/testdata/job.json",
    "content": "{\n  \"apiVersion\": \"batch/v1\",\n  \"kind\": \"Job\",\n  \"metadata\": {\n    \"creationTimestamp\": \"2019-08-30T15:33:02Z\",\n    \"labels\": {\n      \"controller-uid\": \"7473e6d0-cb3b-11e9-990f-42010a800218\",\n      \"job-name\": \"hello-1567179180\"\n    },\n    \"name\": \"hello-1567179180\",\n    \"namespace\": \"default\",\n    \"ownerReferences\": [\n      {\n        \"apiVersion\": \"batch/v1beta1\",\n        \"blockOwnerDeletion\": true,\n        \"controller\": true,\n        \"kind\": \"CronJob\",\n        \"name\": \"hello\",\n        \"uid\": \"7f0b856c-cb39-11e9-990f-42010a800218\"\n      }\n    ],\n    \"resourceVersion\": \"49735780\",\n    \"selfLink\": \"/apis/batch/v1/namespaces/default/jobs/hello-1567179180\",\n    \"uid\": \"7473e6d0-cb3b-11e9-990f-42010a800218\"\n  },\n  \"spec\": {\n    \"backoffLimit\": 6,\n    \"completions\": 1,\n    \"parallelism\": 1,\n    \"selector\": {\n      \"matchLabels\": {\n        \"controller-uid\": \"7473e6d0-cb3b-11e9-990f-42010a800218\"\n      }\n    },\n    \"template\": {\n      \"metadata\": {\n        \"creationTimestamp\": null,\n        \"labels\": {\n          \"controller-uid\": \"7473e6d0-cb3b-11e9-990f-42010a800218\",\n          \"job-name\": \"hello-1567179180\"\n        }\n      },\n      \"spec\": {\n        \"containers\": [\n          {\n            \"args\": [\n              \"/bin/bash\",\n              \"-c\",\n              \"for i in {1..5}; do echo c1 $i; sleep 1; done\"\n            ],\n            \"image\": \"blang/busybox-bash\",\n            \"imagePullPolicy\": \"Always\",\n            \"name\": \"c1\",\n            \"resources\": {},\n            \"terminationMessagePath\": \"/dev/termination-log\",\n            \"terminationMessagePolicy\": \"File\"\n          }\n        ],\n        \"dnsPolicy\": \"ClusterFirst\",\n        \"restartPolicy\": \"OnFailure\",\n        \"schedulerName\": \"default-scheduler\",\n        \"securityContext\": {},\n        \"terminationGracePeriodSeconds\": 30\n      }\n    }\n  },\n  \"status\": {\n    \"completionTime\": \"2019-08-30T15:33:10Z\",\n    \"conditions\": [\n      {\n        \"lastProbeTime\": \"2019-08-30T15:33:10Z\",\n        \"lastTransitionTime\": \"2019-08-30T15:33:10Z\",\n        \"status\": \"True\",\n        \"type\": \"Complete\"\n      }\n    ],\n    \"startTime\": \"2019-08-30T15:33:02Z\",\n    \"succeeded\": 1\n  }\n}"
  },
  {
    "path": "internal/render/testdata/no.json",
    "content": "{\n  \"apiVersion\": \"v1\",\n  \"kind\": \"Node\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubeadm.alpha.kubernetes.io/cri-socket\": \"/var/run/dockershim.sock\",\n      \"node.alpha.kubernetes.io/ttl\": \"0\",\n      \"volumes.kubernetes.io/controller-managed-attach-detach\": \"true\"\n    },\n    \"creationTimestamp\": \"2019-08-26T21:52:09Z\",\n    \"labels\": {\n      \"beta.kubernetes.io/arch\": \"amd64\",\n      \"beta.kubernetes.io/os\": \"linux\",\n      \"kubernetes.io/arch\": \"amd64\",\n      \"kubernetes.io/hostname\": \"minikube\",\n      \"kubernetes.io/os\": \"linux\",\n      \"node-role.kubernetes.io/master\": \"\"\n    },\n    \"name\": \"minikube\",\n    \"resourceVersion\": \"500588\",\n    \"selfLink\": \"/api/v1/nodes/minikube\",\n    \"uid\": \"3a554aa2-fee7-435b-ae1b-e67bdaac069a\"\n  },\n  \"spec\": {},\n  \"status\": {\n    \"addresses\": [\n      {\n        \"address\": \"192.168.64.107\",\n        \"type\": \"InternalIP\"\n      },\n      {\n        \"address\": \"minikube\",\n        \"type\": \"Hostname\"\n      }\n    ],\n    \"allocatable\": {\n      \"cpu\": \"4\",\n      \"ephemeral-storage\": \"15625027559\",\n      \"hugepages-2Mi\": \"0\",\n      \"memory\": \"8063156Ki\",\n      \"pods\": \"110\"\n    },\n    \"capacity\": {\n      \"cpu\": \"4\",\n      \"ephemeral-storage\": \"16954240Ki\",\n      \"hugepages-2Mi\": \"0\",\n      \"memory\": \"8165556Ki\",\n      \"pods\": \"110\"\n    },\n    \"conditions\": [\n      {\n        \"lastHeartbeatTime\": \"2019-08-31T04:43:11Z\",\n        \"lastTransitionTime\": \"2019-08-26T21:52:06Z\",\n        \"message\": \"kubelet has sufficient memory available\",\n        \"reason\": \"KubeletHasSufficientMemory\",\n        \"status\": \"False\",\n        \"type\": \"MemoryPressure\"\n      },\n      {\n        \"lastHeartbeatTime\": \"2019-08-31T04:43:11Z\",\n        \"lastTransitionTime\": \"2019-08-26T21:52:06Z\",\n        \"message\": \"kubelet has no disk pressure\",\n        \"reason\": \"KubeletHasNoDiskPressure\",\n        \"status\": \"False\",\n        \"type\": \"DiskPressure\"\n      },\n      {\n        \"lastHeartbeatTime\": \"2019-08-31T04:43:11Z\",\n        \"lastTransitionTime\": \"2019-08-26T21:52:06Z\",\n        \"message\": \"kubelet has sufficient PID available\",\n        \"reason\": \"KubeletHasSufficientPID\",\n        \"status\": \"False\",\n        \"type\": \"PIDPressure\"\n      },\n      {\n        \"lastHeartbeatTime\": \"2019-08-31T04:43:11Z\",\n        \"lastTransitionTime\": \"2019-08-26T21:52:06Z\",\n        \"message\": \"kubelet is posting ready status\",\n        \"reason\": \"KubeletReady\",\n        \"status\": \"True\",\n        \"type\": \"Ready\"\n      }\n    ],\n    \"daemonEndpoints\": {\n      \"kubeletEndpoint\": {\n        \"Port\": 10250\n      }\n    },\n    \"images\": [\n      {\n        \"names\": [\n          \"quay.io/kubernetes-ingress-controller/nginx-ingress-controller@sha256:464db4880861bd9d1e74e67a4a9c975a6e74c1e9968776d8d4cc73492a56dfa5\",\n          \"quay.io/kubernetes-ingress-controller/nginx-ingress-controller:0.25.0\"\n        ],\n        \"sizeBytes\": 508299926\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/etcd@sha256:17da501f5d2a675be46040422a27b7cc21b8a43895ac998b171db1c346f361f7\",\n          \"k8s.gcr.io/etcd:3.3.10\"\n        ],\n        \"sizeBytes\": 258116302\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/kube-apiserver@sha256:5fae387bacf1def6c3915b4a3035cf8c8a4d06158b2e676721776d3d4afc05a2\",\n          \"k8s.gcr.io/kube-apiserver:v1.15.2\"\n        ],\n        \"sizeBytes\": 206823358\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/kube-controller-manager@sha256:7d3fc48cf83aa0a7b8f129fa4255bb5530908e1a5b194be269ea8329b48e9598\",\n          \"k8s.gcr.io/kube-controller-manager:v1.15.2\"\n        ],\n        \"sizeBytes\": 158718526\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/kubernetes-dashboard-amd64:v1.10.1\"\n        ],\n        \"sizeBytes\": 121711221\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/nginx-slim@sha256:8b4501fe0fe221df663c22e16539f399e89594552f400408303c42f3dd8d0e52\",\n          \"k8s.gcr.io/nginx-slim:0.8\"\n        ],\n        \"sizeBytes\": 110487599\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/kube-addon-manager:v9.0\"\n        ],\n        \"sizeBytes\": 83077558\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/kube-proxy@sha256:626f983f25f8b7799ca7ab001fd0985a72c2643c0acb877d2888c0aa4fcbdf56\",\n          \"k8s.gcr.io/kube-proxy:v1.15.2\"\n        ],\n        \"sizeBytes\": 82408284\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/kube-scheduler@sha256:8fd3c3251f07234a234469e201900e4274726f1fe0d5dc6fb7da911f1c851a1a\",\n          \"k8s.gcr.io/kube-scheduler:v1.15.2\"\n        ],\n        \"sizeBytes\": 81107582\n      },\n      {\n        \"names\": [\n          \"gcr.io/k8s-minikube/storage-provisioner:v1.8.1\"\n        ],\n        \"sizeBytes\": 80815640\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/k8s-dns-kube-dns-amd64:1.14.13\"\n        ],\n        \"sizeBytes\": 51157394\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/k8s-dns-sidecar-amd64:1.14.13\"\n        ],\n        \"sizeBytes\": 42852039\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/metrics-server-amd64@sha256:49a9f12f7067d11f42c803dbe61ed2c1299959ad85cb315b25ff7eef8e6b8892\",\n          \"k8s.gcr.io/metrics-server-amd64:v0.2.1\"\n        ],\n        \"sizeBytes\": 42541759\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/k8s-dns-dnsmasq-nanny-amd64:1.14.13\"\n        ],\n        \"sizeBytes\": 41372492\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/coredns@sha256:02382353821b12c21b062c59184e227e001079bb13ebd01f9d3270ba0fcbf1e4\",\n          \"k8s.gcr.io/coredns:1.3.1\"\n        ],\n        \"sizeBytes\": 40303560\n      },\n      {\n        \"names\": [\n          \"blang/busybox-bash@sha256:b4675e303209bfdaeb6cad4c0c90ec3ba2cda85a75b5d965daa91bca86d0d77c\",\n          \"blang/busybox-bash:latest\"\n        ],\n        \"sizeBytes\": 5912460\n      },\n      {\n        \"names\": [\n          \"k8s.gcr.io/pause@sha256:f78411e19d84a252e53bff71a4407a5686c46983a2c2eeed83929b888179acea\",\n          \"k8s.gcr.io/pause:3.1\"\n        ],\n        \"sizeBytes\": 742472\n      }\n    ],\n    \"nodeInfo\": {\n      \"architecture\": \"amd64\",\n      \"bootID\": \"97588c94-edf3-420d-b5ef-226d5a27d348\",\n      \"containerRuntimeVersion\": \"docker://18.9.8\",\n      \"kernelVersion\": \"4.15.0\",\n      \"kubeProxyVersion\": \"v1.15.2\",\n      \"kubeletVersion\": \"v1.15.2\",\n      \"machineID\": \"fc8b6c7d6c8449bf9066f42449d97619\",\n      \"operatingSystem\": \"linux\",\n      \"osImage\": \"Buildroot 2018.05.3\",\n      \"systemUUID\": \"98F211E9-0000-0000-AC5E-AC87A33863C5\"\n    }\n  }\n}"
  },
  {
    "path": "internal/render/testdata/np.json",
    "content": "{\n  \"apiVersion\": \"networking.k8s.io/v1\",\n  \"kind\": \"NetworkPolicy\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"networking.k8s.io/v1\\\",\\\"kind\\\":\\\"NetworkPolicy\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"fred\\\",\\\"namespace\\\":\\\"default\\\"},\\\"spec\\\":{\\\"egress\\\":[{\\\"ports\\\":[{\\\"port\\\":5978,\\\"protocol\\\":\\\"TCP\\\"}],\\\"to\\\":[{\\\"ipBlock\\\":{\\\"cidr\\\":\\\"10.0.0.0/24\\\"}}]}],\\\"ingress\\\":[{\\\"from\\\":[{\\\"ipBlock\\\":{\\\"cidr\\\":\\\"172.17.0.0/16\\\",\\\"except\\\":[\\\"172.17.1.0/24\\\",\\\"172.17.3.0/24\\\",\\\"172.17.4.0/24\\\"]}},{\\\"namespaceSelector\\\":{\\\"matchLabels\\\":{\\\"app\\\":\\\"blee\\\"}}},{\\\"podSelector\\\":{\\\"matchLabels\\\":{\\\"app\\\":\\\"fred\\\"}}}],\\\"ports\\\":[{\\\"port\\\":6379,\\\"protocol\\\":\\\"TCP\\\"}]}],\\\"podSelector\\\":{\\\"matchLabels\\\":{\\\"app\\\":\\\"nginx\\\"}},\\\"policyTypes\\\":[\\\"Ingress\\\",\\\"Egress\\\"]}}\\n\"\n    },\n    \"creationTimestamp\": \"2019-08-27T19:07:20Z\",\n    \"generation\": 2,\n    \"name\": \"fred\",\n    \"namespace\": \"default\",\n    \"resourceVersion\": \"48999995\",\n    \"selfLink\": \"/apis/networking.k8s.io/v1/namespaces/default/networkpolicies/fred\",\n    \"uid\": \"e4aada4d-c8fd-11e9-990f-42010a800218\"\n  },\n  \"spec\": {\n    \"egress\": [\n      {\n        \"ports\": [\n          {\n            \"port\": 5978,\n            \"protocol\": \"TCP\"\n          }\n        ],\n        \"to\": [\n          {\n            \"ipBlock\": {\n              \"cidr\": \"10.0.0.0/24\"\n            }\n          }\n        ]\n      }\n    ],\n    \"ingress\": [\n      {\n        \"from\": [\n          {\n            \"ipBlock\": {\n              \"cidr\": \"172.17.0.0/16\",\n              \"except\": [\n                \"172.17.1.0/24\",\n                \"172.17.3.0/24\",\n                \"172.17.4.0/24\"\n              ]\n            }\n          },\n          {\n            \"namespaceSelector\": {\n              \"matchLabels\": {\n                \"app\": \"blee\"\n              }\n            }\n          },\n          {\n            \"podSelector\": {\n              \"matchLabels\": {\n                \"app\": \"fred\"\n              }\n            }\n          }\n        ],\n        \"ports\": [\n          {\n            \"port\": 6379,\n            \"protocol\": \"TCP\"\n          }\n        ]\n      }\n    ],\n    \"podSelector\": {\n      \"matchLabels\": {\n        \"app\": \"nginx\"\n      }\n    },\n    \"policyTypes\": [\n      \"Ingress\",\n      \"Egress\"\n    ]\n  }\n}"
  },
  {
    "path": "internal/render/testdata/ns.json",
    "content": "{\n  \"apiVersion\": \"v1\",\n  \"kind\": \"Namespace\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"v1\\\",\\\"kind\\\":\\\"Namespace\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"kube-system\\\",\\\"namespace\\\":\\\"\\\"}}\\n\"\n    },\n    \"creationTimestamp\": \"2019-02-05T22:03:54Z\",\n    \"name\": \"kube-system\",\n    \"resourceVersion\": \"36\",\n    \"selfLink\": \"/api/v1/namespaces/kube-system\",\n    \"uid\": \"ed757b6f-2991-11e9-81cd-42010a80005b\"\n  },\n  \"spec\": {\n    \"finalizers\": [\n      \"kubernetes\"\n    ]\n  },\n  \"status\": {\n    \"phase\": \"Active\"\n  }\n}"
  },
  {
    "path": "internal/render/testdata/p1.json",
    "content": "{\n  \"apiVersion\": \"v1\",\n  \"kind\": \"Pod\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/restartedAt\": \"2019-12-31T12:26:47-07:00\"\n    },\n    \"creationTimestamp\": \"2019-12-31T19:27:22Z\",\n    \"generateName\": \"nginx-7fb78fb6d8-\",\n    \"labels\": {\n      \"app\": \"nginx\",\n      \"pod-template-hash\": \"7fb78fb6d8\"\n    },\n    \"name\": \"nginx-7fb78fb6d8-2w75j\",\n    \"namespace\": \"default\",\n    \"ownerReferences\": [\n      {\n        \"apiVersion\": \"apps/v1\",\n        \"blockOwnerDeletion\": true,\n        \"controller\": true,\n        \"kind\": \"ReplicaSet\",\n        \"name\": \"nginx-7fb78fb6d8\",\n        \"uid\": \"7ccd0600-2c03-11ea-883f-42010a800044\"\n      }\n    ],\n    \"resourceVersion\": \"87290191\",\n    \"selfLink\": \"/api/v1/namespaces/default/pods/nginx-7fb78fb6d8-2w75j\",\n    \"uid\": \"91bb1cf2-2c03-11ea-883f-42010a800044\"\n  },\n  \"spec\": {\n    \"containers\": [\n      {\n        \"image\": \"k8s.gcr.io/nginx-slim:0.8\",\n        \"imagePullPolicy\": \"IfNotPresent\",\n        \"name\": \"nginx\",\n        \"ports\": [\n          {\n            \"containerPort\": 80,\n            \"protocol\": \"TCP\"\n          }\n        ],\n        \"resources\": {\n          \"limits\": {\n            \"cpu\": \"200m\",\n            \"memory\": \"20Mi\"\n          },\n          \"requests\": {\n            \"cpu\": \"200m\",\n            \"memory\": \"20Mi\"\n          }\n        },\n        \"terminationMessagePath\": \"/dev/termination-log\",\n        \"terminationMessagePolicy\": \"File\",\n        \"volumeMounts\": [\n          {\n            \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\",\n            \"name\": \"default-token-dsl46\",\n            \"readOnly\": true\n          }\n        ]\n      }\n    ],\n    \"dnsPolicy\": \"ClusterFirst\",\n    \"enableServiceLinks\": true,\n    \"nodeName\": \"gke-k9s-default-pool-0fa2fb89-lbtf\",\n    \"priority\": 0,\n    \"restartPolicy\": \"Always\",\n    \"schedulerName\": \"default-scheduler\",\n    \"securityContext\": {},\n    \"serviceAccount\": \"default\",\n    \"serviceAccountName\": \"default\",\n    \"terminationGracePeriodSeconds\": 30,\n    \"tolerations\": [\n      {\n        \"effect\": \"NoExecute\",\n        \"key\": \"node.kubernetes.io/not-ready\",\n        \"operator\": \"Exists\",\n        \"tolerationSeconds\": 300\n      },\n      {\n        \"effect\": \"NoExecute\",\n        \"key\": \"node.kubernetes.io/unreachable\",\n        \"operator\": \"Exists\",\n        \"tolerationSeconds\": 300\n      }\n    ],\n    \"volumes\": [\n      {\n        \"name\": \"default-token-dsl46\",\n        \"secret\": {\n          \"defaultMode\": 420,\n          \"secretName\": \"default-token-dsl46\"\n        }\n      }\n    ]\n  },\n  \"status\": {\n    \"conditions\": [\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-12-31T19:27:23Z\",\n        \"status\": \"True\",\n        \"type\": \"Initialized\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-12-31T19:27:25Z\",\n        \"status\": \"True\",\n        \"type\": \"Ready\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-12-31T19:27:25Z\",\n        \"status\": \"True\",\n        \"type\": \"ContainersReady\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-12-31T19:27:22Z\",\n        \"status\": \"True\",\n        \"type\": \"PodScheduled\"\n      }\n    ],\n    \"containerStatuses\": [\n      {\n        \"containerID\": \"docker://90e0abf7a779dd76d36038883312baed57a8351428a1d6340df3cff698f51809\",\n        \"image\": \"k8s.gcr.io/nginx-slim:0.8\",\n        \"imageID\": \"docker-pullable://k8s.gcr.io/nginx-slim@sha256:8b4501fe0fe221df663c22e16539f399e89594552f400408303c42f3dd8d0e52\",\n        \"lastState\": {},\n        \"name\": \"nginx\",\n        \"ready\": true,\n        \"restartCount\": 0,\n        \"state\": {\n          \"running\": {\n            \"startedAt\": \"2019-12-31T19:27:24Z\"\n          }\n        }\n      }\n    ],\n    \"hostIP\": \"10.128.0.15\",\n    \"phase\": \"Running\",\n    \"podIP\": \"10.44.0.229\",\n    \"qosClass\": \"Guaranteed\",\n    \"startTime\": \"2019-12-31T19:27:23Z\"\n  }\n}"
  },
  {
    "path": "internal/render/testdata/pdb.json",
    "content": "{\n  \"apiVersion\": \"policy/v1\",\n  \"kind\": \"PodDisruptionBudget\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"policy/v1\\\",\\\"kind\\\":\\\"PodDisruptionBudget\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"fred\\\",\\\"namespace\\\":\\\"default\\\"},\\\"spec\\\":{\\\"minAvailable\\\":2,\\\"selector\\\":{\\\"matchLabels\\\":{\\\"app\\\":\\\"nginx\\\"}}}}\\n\"\n    },\n    \"creationTimestamp\": \"2019-08-31T03:48:10Z\",\n    \"generation\": 1,\n    \"name\": \"fred\",\n    \"namespace\": \"default\",\n    \"resourceVersion\": \"49885429\",\n    \"selfLink\": \"/apis/policy/v1/namespaces/default/poddisruptionbudgets/fred\",\n    \"uid\": \"26b6cf70-cba2-11e9-990f-42010a800218\"\n  },\n  \"spec\": {\n    \"minAvailable\": 2,\n    \"selector\": {\n      \"matchLabels\": {\n        \"app\": \"nginx\"\n      }\n    }\n  },\n  \"status\": {\n    \"currentHealthy\": 0,\n    \"desiredHealthy\": 2,\n    \"disruptionsAllowed\": 0,\n    \"expectedPods\": 0,\n    \"observedGeneration\": 1\n  }\n}"
  },
  {
    "path": "internal/render/testdata/po.json",
    "content": "{\n  \"apiVersion\": \"v1\",\n  \"kind\": \"Pod\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"v1\\\",\\\"kind\\\":\\\"Pod\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"nginx\\\",\\\"namespace\\\":\\\"default\\\"},\\\"spec\\\":{\\\"containers\\\":[{\\\"image\\\":\\\"nginx:alpine\\\",\\\"name\\\":\\\"nginx\\\",\\\"ports\\\":[{\\\"containerPort\\\":80}],\\\"volumeMounts\\\":[{\\\"mountPath\\\":\\\"/usr/share/nginx/html\\\",\\\"name\\\":\\\"index\\\"}]}],\\\"terminationGracePeriodSeconds\\\":0,\\\"volumes\\\":[{\\\"name\\\":\\\"index\\\",\\\"persistentVolumeClaim\\\":{\\\"claimName\\\":\\\"web\\\"}}]}}\\n\"\n    },\n    \"creationTimestamp\": \"2019-08-09T05:12:19Z\",\n    \"name\": \"nginx\",\n    \"namespace\": \"default\",\n    \"resourceVersion\": \"1482816\",\n    \"selfLink\": \"/api/v1/namespaces/default/pods/nginx\",\n    \"uid\": \"614908ed-415b-4506-8370-e3e36fa8cc13\"\n  },\n  \"spec\": {\n    \"containers\": [\n      {\n        \"image\": \"nginx:alpine\",\n        \"imagePullPolicy\": \"IfNotPresent\",\n        \"name\": \"nginx\",\n        \"ports\": [\n          {\n            \"containerPort\": 80,\n            \"protocol\": \"TCP\"\n          }\n        ],\n        \"resources\": {\n          \"limits\": {\n            \"memory\": \"170Mi\"\n          },\n          \"requests\": {\n            \"cpu\": \"100m\",\n            \"memory\": \"70Mi\"\n          }\n        },\n        \"terminationMessagePath\": \"/dev/termination-log\",\n        \"terminationMessagePolicy\": \"File\",\n        \"volumeMounts\": [\n          {\n            \"mountPath\": \"/usr/share/nginx/html\",\n            \"name\": \"index\"\n          },\n          {\n            \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\",\n            \"name\": \"default-token-9ph8s\",\n            \"readOnly\": true\n          }\n        ]\n      }\n    ],\n    \"dnsPolicy\": \"ClusterFirst\",\n    \"enableServiceLinks\": true,\n    \"nodeName\": \"minikube\",\n    \"priority\": 0,\n    \"restartPolicy\": \"Always\",\n    \"schedulerName\": \"default-scheduler\",\n    \"securityContext\": {},\n    \"serviceAccount\": \"default\",\n    \"serviceAccountName\": \"default\",\n    \"terminationGracePeriodSeconds\": 0,\n    \"tolerations\": [\n      {\n        \"effect\": \"NoExecute\",\n        \"key\": \"node.kubernetes.io/not-ready\",\n        \"operator\": \"Exists\",\n        \"tolerationSeconds\": 300\n      },\n      {\n        \"effect\": \"NoExecute\",\n        \"key\": \"node.kubernetes.io/unreachable\",\n        \"operator\": \"Exists\",\n        \"tolerationSeconds\": 300\n      }\n    ],\n    \"volumes\": [\n      {\n        \"name\": \"index\",\n        \"persistentVolumeClaim\": {\n          \"claimName\": \"web\"\n        }\n      },\n      {\n        \"name\": \"default-token-9ph8s\",\n        \"secret\": {\n          \"defaultMode\": 420,\n          \"secretName\": \"default-token-9ph8s\"\n        }\n      }\n    ]\n  },\n  \"status\": {\n    \"conditions\": [\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-08-09T05:12:19Z\",\n        \"status\": \"True\",\n        \"type\": \"Initialized\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-08-09T05:12:21Z\",\n        \"status\": \"True\",\n        \"type\": \"Ready\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-08-09T05:12:21Z\",\n        \"status\": \"True\",\n        \"type\": \"ContainersReady\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-08-09T05:12:19Z\",\n        \"status\": \"True\",\n        \"type\": \"PodScheduled\"\n      }\n    ],\n    \"containerStatuses\": [\n      {\n        \"containerID\": \"docker://421bd26d6c682f14b5ea1dcaf06e14a509b2b702fc7793e820520eb1e28e2eaf\",\n        \"image\": \"nginx:alpine\",\n        \"imageID\": \"docker-pullable://nginx@sha256:482ead44b2203fa32b3390abdaf97cbdc8ad15c07fb03a3e68d7c35a19ad7595\",\n        \"lastState\": {},\n        \"name\": \"nginx\",\n        \"ready\": true,\n        \"restartCount\": 0,\n        \"state\": {\n          \"running\": {\n            \"startedAt\": \"2019-08-09T05:12:20Z\"\n          }\n        }\n      }\n    ],\n    \"hostIP\": \"192.168.64.104\",\n    \"phase\": \"Running\",\n    \"podIP\": \"172.17.0.6\",\n    \"qosClass\": \"BestEffort\",\n    \"startTime\": \"2019-08-09T05:12:19Z\"\n  }\n}"
  },
  {
    "path": "internal/render/testdata/po_init.json",
    "content": "{\n  \"apiVersion\": \"v1\",\n  \"kind\": \"Pod\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"v1\\\",\\\"kind\\\":\\\"Pod\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"nginx\\\",\\\"namespace\\\":\\\"default\\\"},\\\"spec\\\":{\\\"containers\\\":[{\\\"image\\\":\\\"nginx:alpine\\\",\\\"name\\\":\\\"nginx\\\",\\\"ports\\\":[{\\\"containerPort\\\":80}],\\\"volumeMounts\\\":[{\\\"mountPath\\\":\\\"/usr/share/nginx/html\\\",\\\"name\\\":\\\"index\\\"}]}],\\\"terminationGracePeriodSeconds\\\":0,\\\"volumes\\\":[{\\\"name\\\":\\\"index\\\",\\\"persistentVolumeClaim\\\":{\\\"claimName\\\":\\\"web\\\"}}]}}\\n\"\n    },\n    \"creationTimestamp\": \"2019-08-09T05:12:19Z\",\n    \"name\": \"nginx\",\n    \"namespace\": \"default\",\n    \"resourceVersion\": \"1482816\",\n    \"selfLink\": \"/api/v1/namespaces/default/pods/nginx\",\n    \"uid\": \"614908ed-415b-4506-8370-e3e36fa8cc13\"\n  },\n  \"spec\": {\n    \"initContainers\": [\n      {\n        \"image\": \"nginx:alpine\",\n        \"imagePullPolicy\": \"IfNotPresent\",\n        \"name\": \"ic1\",\n        \"ports\": [\n          {\n            \"containerPort\": 80,\n            \"protocol\": \"TCP\"\n          }\n        ],\n        \"resources\": {\n          \"limits\": {\n            \"memory\": \"170Mi\"\n          },\n          \"requests\": {\n            \"cpu\": \"100m\",\n            \"memory\": \"70Mi\"\n          }\n        },\n        \"terminationMessagePath\": \"/dev/termination-log\",\n        \"terminationMessagePolicy\": \"File\",\n        \"volumeMounts\": [\n          {\n            \"mountPath\": \"/usr/share/nginx/html\",\n            \"name\": \"index\"\n          },\n          {\n            \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\",\n            \"name\": \"default-token-9ph8s\",\n            \"readOnly\": true\n          }\n        ]\n      }\n    ],\n    \"containers\": [\n      {\n        \"image\": \"nginx:alpine\",\n        \"imagePullPolicy\": \"IfNotPresent\",\n        \"name\": \"nginx\",\n        \"ports\": [\n          {\n            \"containerPort\": 80,\n            \"protocol\": \"TCP\"\n          }\n        ],\n        \"resources\": {\n          \"limits\": {\n            \"memory\": \"170Mi\"\n          },\n          \"requests\": {\n            \"cpu\": \"100m\",\n            \"memory\": \"70Mi\"\n          }\n        },\n        \"terminationMessagePath\": \"/dev/termination-log\",\n        \"terminationMessagePolicy\": \"File\",\n        \"volumeMounts\": [\n          {\n            \"mountPath\": \"/usr/share/nginx/html\",\n            \"name\": \"index\"\n          },\n          {\n            \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\",\n            \"name\": \"default-token-9ph8s\",\n            \"readOnly\": true\n          }\n        ]\n      }\n    ],\n    \"dnsPolicy\": \"ClusterFirst\",\n    \"enableServiceLinks\": true,\n    \"nodeName\": \"minikube\",\n    \"priority\": 0,\n    \"restartPolicy\": \"Always\",\n    \"schedulerName\": \"default-scheduler\",\n    \"securityContext\": {},\n    \"serviceAccount\": \"default\",\n    \"serviceAccountName\": \"default\",\n    \"terminationGracePeriodSeconds\": 0,\n    \"tolerations\": [\n      {\n        \"effect\": \"NoExecute\",\n        \"key\": \"node.kubernetes.io/not-ready\",\n        \"operator\": \"Exists\",\n        \"tolerationSeconds\": 300\n      },\n      {\n        \"effect\": \"NoExecute\",\n        \"key\": \"node.kubernetes.io/unreachable\",\n        \"operator\": \"Exists\",\n        \"tolerationSeconds\": 300\n      }\n    ],\n    \"volumes\": [\n      {\n        \"name\": \"index\",\n        \"persistentVolumeClaim\": {\n          \"claimName\": \"web\"\n        }\n      },\n      {\n        \"name\": \"default-token-9ph8s\",\n        \"secret\": {\n          \"defaultMode\": 420,\n          \"secretName\": \"default-token-9ph8s\"\n        }\n      }\n    ]\n  },\n  \"status\": {\n    \"conditions\": [\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-08-09T05:12:19Z\",\n        \"status\": \"True\",\n        \"type\": \"Initialized\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-08-09T05:12:21Z\",\n        \"status\": \"True\",\n        \"type\": \"Ready\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-08-09T05:12:21Z\",\n        \"status\": \"True\",\n        \"type\": \"ContainersReady\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-08-09T05:12:19Z\",\n        \"status\": \"True\",\n        \"type\": \"PodScheduled\"\n      }\n    ],\n    \"containerStatuses\": [\n      {\n        \"containerID\": \"docker://421bd26d6c682f14b5ea1dcaf06e14a509b2b702fc7793e820520eb1e28e2eaf\",\n        \"image\": \"nginx:alpine\",\n        \"imageID\": \"docker-pullable://nginx@sha256:482ead44b2203fa32b3390abdaf97cbdc8ad15c07fb03a3e68d7c35a19ad7595\",\n        \"lastState\": {},\n        \"name\": \"nginx\",\n        \"ready\": true,\n        \"restartCount\": 0,\n        \"state\": {\n          \"running\": {\n            \"startedAt\": \"2019-08-09T05:12:20Z\"\n          }\n        }\n      }\n    ],\n    \"initContainerStatuses\": [\n      {\n        \"containerID\": \"docker://421bd26d6c682f14b5ea1dcaf06e14a509b2b702fc7793e820520eb1e28e2eaf\",\n        \"image\": \"nginx:alpine\",\n        \"imageID\": \"docker-pullable://nginx@sha256:482ead44b2203fa32b3390abdaf97cbdc8ad15c07fb03a3e68d7c35a19ad7595\",\n        \"lastState\": {},\n        \"name\": \"ic1\",\n        \"ready\": true,\n        \"restartCount\": 0,\n        \"state\": {\n          \"running\": {\n            \"startedAt\": \"2019-08-09T05:12:20Z\"\n          }\n        }\n      }\n    ],\n    \"hostIP\": \"192.168.64.104\",\n    \"phase\": \"Running\",\n    \"podIP\": \"172.17.0.6\",\n    \"qosClass\": \"BestEffort\",\n    \"startTime\": \"2019-08-09T05:12:19Z\"\n  }\n}"
  },
  {
    "path": "internal/render/testdata/po_sidecar.json",
    "content": "{\r\n    \"apiVersion\": \"v1\",\r\n    \"kind\": \"Pod\",\r\n    \"metadata\": {\r\n        \"creationTimestamp\": \"2024-08-24T01:54:32Z\",\r\n        \"name\": \"sleep\",\r\n        \"namespace\": \"default\",\r\n        \"resourceVersion\": \"17852\",\r\n        \"uid\": \"35079257-0ffb-4b09-b2c1-3c0d416f2523\"\r\n    },\r\n    \"spec\": {\r\n        \"containers\": [\r\n            {\r\n                \"command\": [\r\n                    \"/bin/sleep\",\r\n                    \"60\"\r\n                ],\r\n                \"image\": \"istio/base\",\r\n                \"imagePullPolicy\": \"Always\",\r\n                \"name\": \"sleep\",\r\n                \"resources\": {\r\n                    \"limits\": {\r\n                        \"cpu\": \"230m\",\r\n                        \"memory\": \"40Mi\"\r\n                    },\r\n                    \"requests\": {\r\n                        \"cpu\": \"30m\",\r\n                        \"memory\": \"10Mi\"\r\n                    }\r\n                },\r\n                \"terminationMessagePath\": \"/dev/termination-log\",\r\n                \"terminationMessagePolicy\": \"File\",\r\n                \"volumeMounts\": [\r\n                    {\r\n                        \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\",\r\n                        \"name\": \"kube-api-access-mphcq\",\r\n                        \"readOnly\": true\r\n                    }\r\n                ]\r\n            }\r\n        ],\r\n        \"dnsPolicy\": \"ClusterFirst\",\r\n        \"enableServiceLinks\": true,\r\n        \"initContainers\": [\r\n            {\r\n                \"command\": [\r\n                    \"/bin/sleep\",\r\n                    \"1\"\r\n                ],\r\n                \"image\": \"istio/base\",\r\n                \"imagePullPolicy\": \"Always\",\r\n                \"name\": \"init\",\r\n                \"resources\": {\r\n                    \"limits\": {\r\n                        \"cpu\": \"333m\",\r\n                        \"memory\": \"333Mi\"\r\n                    },\r\n                    \"requests\": {\r\n                        \"cpu\": \"333m\",\r\n                        \"memory\": \"333Mi\"\r\n                    }\r\n                },\r\n                \"terminationMessagePath\": \"/dev/termination-log\",\r\n                \"terminationMessagePolicy\": \"File\",\r\n                \"volumeMounts\": [\r\n                    {\r\n                        \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\",\r\n                        \"name\": \"kube-api-access-mphcq\",\r\n                        \"readOnly\": true\r\n                    }\r\n                ]\r\n            },\r\n            {\r\n                \"command\": [\r\n                    \"/bin/sleep\",\r\n                    \"60\"\r\n                ],\r\n                \"image\": \"istio/base\",\r\n                \"imagePullPolicy\": \"Always\",\r\n                \"name\": \"sidecar\",\r\n                \"resources\": {\r\n                    \"limits\": {\r\n                        \"cpu\": \"20m\",\r\n                        \"memory\": \"40Mi\"\r\n                    },\r\n                    \"requests\": {\r\n                        \"cpu\": \"20m\",\r\n                        \"memory\": \"40Mi\"\r\n                    }\r\n                },\r\n                \"restartPolicy\": \"Always\",\r\n                \"terminationMessagePath\": \"/dev/termination-log\",\r\n                \"terminationMessagePolicy\": \"File\",\r\n                \"volumeMounts\": [\r\n                    {\r\n                        \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\",\r\n                        \"name\": \"kube-api-access-mphcq\",\r\n                        \"readOnly\": true\r\n                    }\r\n                ]\r\n            }\r\n        ],\r\n        \"nodeName\": \"kind-control-plane\",\r\n        \"preemptionPolicy\": \"PreemptLowerPriority\",\r\n        \"priority\": 0,\r\n        \"restartPolicy\": \"Never\",\r\n        \"schedulerName\": \"default-scheduler\",\r\n        \"securityContext\": {},\r\n        \"serviceAccount\": \"default\",\r\n        \"serviceAccountName\": \"default\",\r\n        \"terminationGracePeriodSeconds\": 30,\r\n        \"tolerations\": [\r\n            {\r\n                \"effect\": \"NoExecute\",\r\n                \"key\": \"node.kubernetes.io/not-ready\",\r\n                \"operator\": \"Exists\",\r\n                \"tolerationSeconds\": 300\r\n            },\r\n            {\r\n                \"effect\": \"NoExecute\",\r\n                \"key\": \"node.kubernetes.io/unreachable\",\r\n                \"operator\": \"Exists\",\r\n                \"tolerationSeconds\": 300\r\n            }\r\n        ],\r\n        \"volumes\": [\r\n            {\r\n                \"name\": \"kube-api-access-mphcq\",\r\n                \"projected\": {\r\n                    \"defaultMode\": 420,\r\n                    \"sources\": [\r\n                        {\r\n                            \"serviceAccountToken\": {\r\n                                \"expirationSeconds\": 3607,\r\n                                \"path\": \"token\"\r\n                            }\r\n                        },\r\n                        {\r\n                            \"configMap\": {\r\n                                \"items\": [\r\n                                    {\r\n                                        \"key\": \"ca.crt\",\r\n                                        \"path\": \"ca.crt\"\r\n                                    }\r\n                                ],\r\n                                \"name\": \"kube-root-ca.crt\"\r\n                            }\r\n                        },\r\n                        {\r\n                            \"downwardAPI\": {\r\n                                \"items\": [\r\n                                    {\r\n                                        \"fieldRef\": {\r\n                                            \"apiVersion\": \"v1\",\r\n                                            \"fieldPath\": \"metadata.namespace\"\r\n                                        },\r\n                                        \"path\": \"namespace\"\r\n                                    }\r\n                                ]\r\n                            }\r\n                        }\r\n                    ]\r\n                }\r\n            }\r\n        ]\r\n    },\r\n    \"status\": {\r\n        \"conditions\": [\r\n            {\r\n                \"lastProbeTime\": null,\r\n                \"lastTransitionTime\": \"2024-08-24T01:54:36Z\",\r\n                \"status\": \"True\",\r\n                \"type\": \"PodReadyToStartContainers\"\r\n            },\r\n            {\r\n                \"lastProbeTime\": null,\r\n                \"lastTransitionTime\": \"2024-08-24T01:54:38Z\",\r\n                \"status\": \"True\",\r\n                \"type\": \"Initialized\"\r\n            },\r\n            {\r\n                \"lastProbeTime\": null,\r\n                \"lastTransitionTime\": \"2024-08-24T01:54:39Z\",\r\n                \"status\": \"True\",\r\n                \"type\": \"Ready\"\r\n            },\r\n            {\r\n                \"lastProbeTime\": null,\r\n                \"lastTransitionTime\": \"2024-08-24T01:54:39Z\",\r\n                \"status\": \"True\",\r\n                \"type\": \"ContainersReady\"\r\n            },\r\n            {\r\n                \"lastProbeTime\": null,\r\n                \"lastTransitionTime\": \"2024-08-24T01:54:32Z\",\r\n                \"status\": \"True\",\r\n                \"type\": \"PodScheduled\"\r\n            }\r\n        ],\r\n        \"containerStatuses\": [\r\n            {\r\n                \"containerID\": \"containerd://a1848056a183e40afe3189fc4920bd565930180ebdf2f9e2daf778ea8105f93e\",\r\n                \"image\": \"docker.io/istio/base:latest\",\r\n                \"imageID\": \"docker.io/istio/base@sha256:61673929bc39a86dca7d978b27fc632d3e590bc59cd8b2f386408751d007c91e\",\r\n                \"lastState\": {},\r\n                \"name\": \"sleep\",\r\n                \"ready\": true,\r\n                \"restartCount\": 0,\r\n                \"started\": true,\r\n                \"state\": {\r\n                    \"running\": {\r\n                        \"startedAt\": \"2024-08-24T01:54:38Z\"\r\n                    }\r\n                },\r\n                \"volumeMounts\": [\r\n                    {\r\n                        \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\",\r\n                        \"name\": \"kube-api-access-mphcq\",\r\n                        \"readOnly\": true,\r\n                        \"recursiveReadOnly\": \"Disabled\"\r\n                    }\r\n                ]\r\n            }\r\n        ],\r\n        \"hostIP\": \"172.18.0.2\",\r\n        \"hostIPs\": [\r\n            {\r\n                \"ip\": \"172.18.0.2\"\r\n            }\r\n        ],\r\n        \"initContainerStatuses\": [\r\n            {\r\n                \"containerID\": \"containerd://75295261e5d751382c9a6ffa4477b84af2934686c360dcba2d8a6b9bc0f8cada\",\r\n                \"image\": \"docker.io/istio/base:latest\",\r\n                \"imageID\": \"docker.io/istio/base@sha256:61673929bc39a86dca7d978b27fc632d3e590bc59cd8b2f386408751d007c91e\",\r\n                \"lastState\": {},\r\n                \"name\": \"init\",\r\n                \"ready\": true,\r\n                \"restartCount\": 0,\r\n                \"started\": false,\r\n                \"state\": {\r\n                    \"terminated\": {\r\n                        \"containerID\": \"containerd://75295261e5d751382c9a6ffa4477b84af2934686c360dcba2d8a6b9bc0f8cada\",\r\n                        \"exitCode\": 0,\r\n                        \"finishedAt\": \"2024-08-24T01:54:37Z\",\r\n                        \"reason\": \"Completed\",\r\n                        \"startedAt\": \"2024-08-24T01:54:35Z\"\r\n                    }\r\n                },\r\n                \"volumeMounts\": [\r\n                    {\r\n                        \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\",\r\n                        \"name\": \"kube-api-access-mphcq\",\r\n                        \"readOnly\": true,\r\n                        \"recursiveReadOnly\": \"Disabled\"\r\n                    }\r\n                ]\r\n            },\r\n            {\r\n                \"containerID\": \"containerd://7a0d216a09630040c5b165c42cc9d2a95d541d95b6ac0a5ca604bf767d1b2cf0\",\r\n                \"image\": \"docker.io/istio/base:latest\",\r\n                \"imageID\": \"docker.io/istio/base@sha256:61673929bc39a86dca7d978b27fc632d3e590bc59cd8b2f386408751d007c91e\",\r\n                \"lastState\": {},\r\n                \"name\": \"sidecar\",\r\n                \"ready\": true,\r\n                \"restartCount\": 0,\r\n                \"started\": true,\r\n                \"state\": {\r\n                    \"running\": {\r\n                        \"startedAt\": \"2024-08-24T01:54:38Z\"\r\n                    }\r\n                },\r\n                \"volumeMounts\": [\r\n                    {\r\n                        \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\",\r\n                        \"name\": \"kube-api-access-mphcq\",\r\n                        \"readOnly\": true,\r\n                        \"recursiveReadOnly\": \"Disabled\"\r\n                    }\r\n                ]\r\n            }\r\n        ],\r\n        \"phase\": \"Running\",\r\n        \"podIP\": \"10.244.0.8\",\r\n        \"podIPs\": [\r\n            {\r\n                \"ip\": \"10.244.0.8\"\r\n            }\r\n        ],\r\n        \"qosClass\": \"Burstable\",\r\n        \"startTime\": \"2024-08-24T01:54:32Z\"\r\n    }\r\n}"
  },
  {
    "path": "internal/render/testdata/pv.json",
    "content": "{\n  \"apiVersion\": \"v1\",\n  \"kind\": \"PersistentVolume\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubernetes.io/createdby\": \"gce-pd-dynamic-provisioner\",\n      \"pv.kubernetes.io/bound-by-controller\": \"yes\",\n      \"pv.kubernetes.io/provisioned-by\": \"kubernetes.io/gce-pd\"\n    },\n    \"creationTimestamp\": \"2019-06-05T00:08:24Z\",\n    \"finalizers\": [\n      \"kubernetes.io/pv-protection\"\n    ],\n    \"labels\": {\n      \"failure-domain.beta.kubernetes.io/region\": \"us-central1\",\n      \"failure-domain.beta.kubernetes.io/zone\": \"us-central1-a\"\n    },\n    \"name\": \"pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b\",\n    \"resourceVersion\": \"26769902\",\n    \"selfLink\": \"/api/v1/persistentvolumes/pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b\",\n    \"uid\": \"093234ed-8726-11e9-a8e8-42010a80015b\"\n  },\n  \"spec\": {\n    \"accessModes\": [\n      \"ReadWriteOnce\"\n    ],\n    \"capacity\": {\n      \"storage\": \"1Gi\"\n    },\n    \"claimRef\": {\n      \"apiVersion\": \"v1\",\n      \"kind\": \"PersistentVolumeClaim\",\n      \"name\": \"www-nginx-sts-1\",\n      \"namespace\": \"default\",\n      \"resourceVersion\": \"26769889\",\n      \"uid\": \"07aa4e2c-8726-11e9-a8e8-42010a80015b\"\n    },\n    \"gcePersistentDisk\": {\n      \"fsType\": \"ext4\",\n      \"pdName\": \"gke-k9s-fd5bf60e-dynam-pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b\"\n    },\n    \"nodeAffinity\": {\n      \"required\": {\n        \"nodeSelectorTerms\": [\n          {\n            \"matchExpressions\": [\n              {\n                \"key\": \"failure-domain.beta.kubernetes.io/zone\",\n                \"operator\": \"In\",\n                \"values\": [\n                  \"us-central1-a\"\n                ]\n              },\n              {\n                \"key\": \"failure-domain.beta.kubernetes.io/region\",\n                \"operator\": \"In\",\n                \"values\": [\n                  \"us-central1\"\n                ]\n              }\n            ]\n          }\n        ]\n      }\n    },\n    \"persistentVolumeReclaimPolicy\": \"Delete\",\n    \"storageClassName\": \"standard\"\n  },\n  \"status\": {\n    \"phase\": \"Bound\"\n  }\n}"
  },
  {
    "path": "internal/render/testdata/pv_terminating.json",
    "content": "{\n  \"apiVersion\": \"v1\",\n  \"kind\": \"PersistentVolume\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubernetes.io/createdby\": \"gce-pd-dynamic-provisioner\",\n      \"pv.kubernetes.io/bound-by-controller\": \"yes\",\n      \"pv.kubernetes.io/provisioned-by\": \"kubernetes.io/gce-pd\"\n    },\n    \"creationTimestamp\": \"2022-06-22T03:45:57Z\",\n    \"deletionGracePeriodSeconds\": 0,\n    \"deletionTimestamp\": \"2022-07-21T14:11:00Z\",\n    \"finalizers\": [\n      \"kubernetes.io/pv-protection\"\n    ],\n    \"labels\": {\n      \"topology.kubernetes.io/region\": \"asia-southeast1\",\n      \"topology.kubernetes.io/zone\": \"asia-southeast1-b\"\n    },\n    \"name\": \"pvc-a4d86f51-916c-476b-83af-b551c91a8ac0\",\n    \"resourceVersion\": \"29037811\",\n    \"uid\": \"aa195b1a-0e00-43e6-aad9-d4b016904930\"\n  },\n  \"spec\": {\n    \"accessModes\": [\n      \"ReadWriteOnce\"\n    ],\n    \"capacity\": {\n      \"storage\": \"1Gi\"\n    },\n    \"claimRef\": {\n      \"apiVersion\": \"v1\",\n      \"kind\": \"PersistentVolumeClaim\",\n      \"name\": \"www-nginx-sts-2\",\n      \"namespace\": \"default\",\n      \"resourceVersion\": \"4028123\",\n      \"uid\": \"a4d86f51-916c-476b-83af-b551c91a8ac0\"\n    },\n    \"gcePersistentDisk\": {\n      \"fsType\": \"ext4\",\n      \"pdName\": \"gke-k9s-fd5bf60e-pvc-a4d86f51-916c-476b-83af-b551c91a8ac0\"\n    },\n    \"nodeAffinity\": {\n      \"required\": {\n        \"nodeSelectorTerms\": [\n          {\n            \"matchExpressions\": [\n              {\n                \"key\": \"topology.kubernetes.io/zone\",\n                \"operator\": \"In\",\n                \"values\": [\n                  \"asia-southeast1-b\"\n                ]\n              },\n              {\n                \"key\": \"topology.kubernetes.io/region\",\n                \"operator\": \"In\",\n                \"values\": [\n                  \"asia-southeast1\"\n                ]\n              }\n            ]\n          }\n        ]\n      }\n    },\n    \"persistentVolumeReclaimPolicy\": \"Delete\",\n    \"storageClassName\": \"standard\",\n    \"volumeMode\": \"Filesystem\"\n  },\n  \"status\": {\n    \"phase\": \"Bound\"\n  }\n}"
  },
  {
    "path": "internal/render/testdata/pvc.json",
    "content": "{\n  \"apiVersion\": \"v1\",\n  \"kind\": \"PersistentVolumeClaim\",\n  \"metadata\": {\n    \"annotations\": {\n      \"pv.kubernetes.io/bind-completed\": \"yes\",\n      \"pv.kubernetes.io/bound-by-controller\": \"yes\",\n      \"volume.beta.kubernetes.io/storage-provisioner\": \"kubernetes.io/gce-pd\"\n    },\n    \"creationTimestamp\": \"2019-06-05T00:08:01Z\",\n    \"finalizers\": [\n      \"kubernetes.io/pvc-protection\"\n    ],\n    \"labels\": {\n      \"app\": \"nginx-sts\"\n    },\n    \"name\": \"www-nginx-sts-0\",\n    \"namespace\": \"default\",\n    \"resourceVersion\": \"26769829\",\n    \"selfLink\": \"/api/v1/namespaces/default/persistentvolumeclaims/www-nginx-sts-0\",\n    \"uid\": \"fbabd470-8725-11e9-a8e8-42010a80015b\"\n  },\n  \"spec\": {\n    \"accessModes\": [\n      \"ReadWriteOnce\"\n    ],\n    \"dataSource\": null,\n    \"resources\": {\n      \"requests\": {\n        \"storage\": \"1Mi\"\n      }\n    },\n    \"storageClassName\": \"standard\",\n    \"volumeName\": \"pvc-fbabd470-8725-11e9-a8e8-42010a80015b\"\n  },\n  \"status\": {\n    \"accessModes\": [\n      \"ReadWriteOnce\"\n    ],\n    \"capacity\": {\n      \"storage\": \"1Gi\"\n    },\n    \"phase\": \"Bound\"\n  }\n}"
  },
  {
    "path": "internal/render/testdata/rb.json",
    "content": "{\n  \"apiVersion\": \"rbac.authorization.k8s.io/v1\",\n  \"kind\": \"RoleBinding\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"rbac.authorization.k8s.io/v1\\\",\\\"kind\\\":\\\"RoleBinding\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"blee\\\",\\\"namespace\\\":\\\"default\\\"},\\\"roleRef\\\":{\\\"apiGroup\\\":\\\"rbac.authorization.k8s.io\\\",\\\"kind\\\":\\\"Role\\\",\\\"name\\\":\\\"blee\\\"},\\\"subjects\\\":[{\\\"kind\\\":\\\"ServiceAccount\\\",\\\"name\\\":\\\"fernand\\\",\\\"namespace\\\":\\\"default\\\"}]}\\n\"\n    },\n    \"creationTimestamp\": \"2019-03-27T22:26:49Z\",\n    \"name\": \"blee\",\n    \"namespace\": \"default\",\n    \"resourceVersion\": \"11177042\",\n    \"selfLink\": \"/apis/rbac.authorization.k8s.io/v1/namespaces/default/rolebindings/blee\",\n    \"uid\": \"69ed0b23-50df-11e9-83c8-42010a800018\"\n  },\n  \"roleRef\": {\n    \"apiGroup\": \"rbac.authorization.k8s.io\",\n    \"kind\": \"Role\",\n    \"name\": \"blee\"\n  },\n  \"subjects\": [\n    {\n      \"kind\": \"ServiceAccount\",\n      \"name\": \"fernand\",\n      \"namespace\": \"default\"\n    }\n  ]\n}"
  },
  {
    "path": "internal/render/testdata/ro.json",
    "content": "{\n  \"apiVersion\": \"rbac.authorization.k8s.io/v1\",\n  \"kind\": \"Role\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"rbac.authorization.k8s.io/v1\\\",\\\"kind\\\":\\\"Role\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"blee\\\",\\\"namespace\\\":\\\"default\\\"},\\\"rules\\\":[{\\\"apiGroups\\\":[\\\"\\\"],\\\"resources\\\":[\\\"pods\\\",\\\"namespaces\\\"],\\\"verbs\\\":[\\\"get\\\",\\\"list\\\",\\\"deletecollection\\\",\\\"patch\\\",\\\"watch\\\"]}]}\\n\"\n    },\n    \"creationTimestamp\": \"2019-05-24T02:58:58Z\",\n    \"name\": \"blee\",\n    \"namespace\": \"default\",\n    \"resourceVersion\": \"23720646\",\n    \"selfLink\": \"/apis/rbac.authorization.k8s.io/v1/namespaces/default/roles/blee\",\n    \"uid\": \"e017e058-7dcf-11e9-b9e0-42010a800003\"\n  },\n  \"rules\": [\n    {\n      \"apiGroups\": [\n        \"\"\n      ],\n      \"resources\": [\n        \"pods\",\n        \"namespaces\"\n      ],\n      \"verbs\": [\n        \"get\",\n        \"list\",\n        \"deletecollection\",\n        \"patch\",\n        \"watch\"\n      ]\n    }\n  ]\n}"
  },
  {
    "path": "internal/render/testdata/rs.json",
    "content": "{\n  \"apiVersion\": \"networking.k8s.io/v1\",\n  \"kind\": \"ReplicaSet\",\n  \"metadata\": {\n    \"annotations\": {\n      \"deployment.kubernetes.io/desired-replicas\": \"1\",\n      \"deployment.kubernetes.io/max-replicas\": \"2\",\n      \"deployment.kubernetes.io/revision\": \"1\"\n    },\n    \"creationTimestamp\": \"2019-07-14T04:54:17Z\",\n    \"generation\": 1,\n    \"labels\": {\n      \"app\": \"icx-db\",\n      \"pod-template-hash\": \"7d4b578979\"\n    },\n    \"name\": \"icx-db-7d4b578979\",\n    \"namespace\": \"icx\",\n    \"ownerReferences\": [\n      {\n        \"apiVersion\": \"apps/v1\",\n        \"blockOwnerDeletion\": true,\n        \"controller\": true,\n        \"kind\": \"Deployment\",\n        \"name\": \"icx-db\",\n        \"uid\": \"6f6143bc-a5f3-11e9-990f-42010a800218\"\n      }\n    ],\n    \"resourceVersion\": \"37116270\",\n    \"selfLink\": \"/apis/networking.k8s.io/v1/namespaces/icx/replicasets/icx-db-7d4b578979\",\n    \"uid\": \"6f637a60-a5f3-11e9-990f-42010a800218\"\n  },\n  \"spec\": {\n    \"replicas\": 1,\n    \"selector\": {\n      \"matchLabels\": {\n        \"app\": \"icx-db\",\n        \"pod-template-hash\": \"7d4b578979\"\n      }\n    },\n    \"template\": {\n      \"metadata\": {\n        \"creationTimestamp\": null,\n        \"labels\": {\n          \"app\": \"icx-db\",\n          \"pod-template-hash\": \"7d4b578979\"\n        }\n      },\n      \"spec\": {\n        \"containers\": [\n          {\n            \"env\": [\n              {\n                \"name\": \"POSTGRES_USER\",\n                \"valueFrom\": {\n                  \"secretKeyRef\": {\n                    \"key\": \"pg_user\",\n                    \"name\": \"icx-creds\"\n                  }\n                }\n              },\n              {\n                \"name\": \"POSTGRES_PASSWORD\",\n                \"valueFrom\": {\n                  \"secretKeyRef\": {\n                    \"key\": \"pg_pwd\",\n                    \"name\": \"icx-creds\"\n                  }\n                }\n              }\n            ],\n            \"image\": \"postgres:9.2-alpine\",\n            \"imagePullPolicy\": \"IfNotPresent\",\n            \"name\": \"icx-db\",\n            \"ports\": [\n              {\n                \"containerPort\": 5432,\n                \"name\": \"client\",\n                \"protocol\": \"TCP\"\n              }\n            ],\n            \"resources\": {\n              \"limits\": {\n                \"cpu\": \"250m\",\n                \"memory\": \"512Mi\"\n              },\n              \"requests\": {\n                \"cpu\": \"250m\",\n                \"memory\": \"256Mi\"\n              }\n            },\n            \"terminationMessagePath\": \"/dev/termination-log\",\n            \"terminationMessagePolicy\": \"File\"\n          }\n        ],\n        \"dnsPolicy\": \"ClusterFirst\",\n        \"restartPolicy\": \"Always\",\n        \"schedulerName\": \"default-scheduler\",\n        \"securityContext\": {},\n        \"terminationGracePeriodSeconds\": 30\n      }\n    }\n  },\n  \"status\": {\n    \"availableReplicas\": 1,\n    \"fullyLabeledReplicas\": 1,\n    \"observedGeneration\": 1,\n    \"readyReplicas\": 1,\n    \"replicas\": 1\n  }\n}"
  },
  {
    "path": "internal/render/testdata/sa.json",
    "content": "{\n  \"apiVersion\": \"v1\",\n  \"kind\": \"ServiceAccount\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"v1\\\",\\\"kind\\\":\\\"ServiceAccount\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"blee\\\",\\\"namespace\\\":\\\"default\\\"},\\\"secrets\\\":[{\\\"name\\\":\\\"blee\\\",\\\"namespace\\\":\\\"default\\\"}]}\\n\"\n    },\n    \"creationTimestamp\": \"2019-06-05T21:56:55Z\",\n    \"name\": \"blee\",\n    \"namespace\": \"default\",\n    \"resourceVersion\": \"27009820\",\n    \"selfLink\": \"/api/v1/namespaces/default/serviceaccounts/blee\",\n    \"uid\": \"d5919410-87dc-11e9-a8e8-42010a80015b\"\n  },\n  \"secrets\": [\n    {\n      \"name\": \"blee\"\n    },\n    {\n      \"name\": \"blee-token-k42bt\"\n    }\n  ]\n}"
  },
  {
    "path": "internal/render/testdata/sc.json",
    "content": "{\n  \"apiVersion\": \"storage.k8s.io/v1\",\n  \"kind\": \"StorageClass\",\n  \"metadata\": {\n    \"annotations\": {\n      \"storageclass.beta.kubernetes.io/is-default-class\": \"true\"\n    },\n    \"creationTimestamp\": \"2019-02-05T22:04:14Z\",\n    \"labels\": {\n      \"addonmanager.kubernetes.io/mode\": \"EnsureExists\",\n      \"kubernetes.io/cluster-service\": \"true\"\n    },\n    \"name\": \"standard\",\n    \"resourceVersion\": \"277\",\n    \"selfLink\": \"/apis/storage.k8s.io/v1/storageclasses/standard\",\n    \"uid\": \"f9d4c94a-2991-11e9-81cd-42010a80005b\"\n  },\n  \"parameters\": {\n    \"type\": \"pd-standard\"\n  },\n  \"provisioner\": \"kubernetes.io/gce-pd\",\n  \"reclaimPolicy\": \"Delete\",\n  \"volumeBindingMode\": \"Immediate\",\n  \"allowVolumeExpansion\": true\n}"
  },
  {
    "path": "internal/render/testdata/sec.json",
    "content": "{\n  \"apiVersion\": \"v1\",\n  \"data\": {\n    \"password\": \"YnVtYmxlYmVldHVuYQo=\",\n    \"token\": \"ZmVybmFuZAo=\"\n  },\n  \"kind\": \"Secret\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"v1\\\",\\\"data\\\":{\\\"password\\\":\\\"YnVtYmxlYmVldHVuYQo=\\\",\\\"token\\\":\\\"ZmVybmFuZAo=\\\"},\\\"kind\\\":\\\"Secret\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"labels\\\":{\\\"app\\\":\\\"fred\\\"},\\\"name\\\":\\\"s1\\\",\\\"namespace\\\":\\\"default\\\"},\\\"type\\\":\\\"Opaque\\\"}\\n\"\n    },\n    \"creationTimestamp\": \"2019-08-30T14:30:50Z\",\n    \"labels\": {\n      \"app\": \"fred\"\n    },\n    \"name\": \"s1\",\n    \"namespace\": \"default\",\n    \"resourceVersion\": \"49724026\",\n    \"selfLink\": \"/api/v1/namespaces/default/secrets/s1\",\n    \"uid\": \"c3e3d3f3-cb32-11e9-990f-42010a800218\"\n  },\n  \"type\": \"Opaque\"\n}"
  },
  {
    "path": "internal/render/testdata/sts.json",
    "content": "{\n  \"apiVersion\": \"apps/v1\",\n  \"kind\": \"StatefulSet\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"apps/v1\\\",\\\"kind\\\":\\\"StatefulSet\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"labels\\\":{\\\"app\\\":\\\"nginx-sts\\\"},\\\"name\\\":\\\"nginx-sts\\\",\\\"namespace\\\":\\\"default\\\"},\\\"spec\\\":{\\\"replicas\\\":2,\\\"selector\\\":{\\\"matchLabels\\\":{\\\"app\\\":\\\"nginx-sts\\\"}},\\\"serviceName\\\":\\\"nginx-sts\\\",\\\"template\\\":{\\\"metadata\\\":{\\\"labels\\\":{\\\"app\\\":\\\"nginx-sts\\\"}},\\\"spec\\\":{\\\"containers\\\":[{\\\"image\\\":\\\"k8s.gcr.io/nginx-slim:0.8\\\",\\\"name\\\":\\\"nginx\\\",\\\"ports\\\":[{\\\"containerPort\\\":80,\\\"name\\\":\\\"web\\\"}],\\\"volumeMounts\\\":[{\\\"mountPath\\\":\\\"/usr/share/nginx/html\\\",\\\"name\\\":\\\"www\\\"}]}]}},\\\"volumeClaimTemplates\\\":[{\\\"metadata\\\":{\\\"name\\\":\\\"www\\\"},\\\"spec\\\":{\\\"accessModes\\\":[\\\"ReadWriteOnce\\\"],\\\"resources\\\":{\\\"requests\\\":{\\\"storage\\\":\\\"1Mi\\\"}}}}]}}\\n\"\n    },\n    \"creationTimestamp\": \"2019-11-30T15:41:42Z\",\n    \"generation\": 5,\n    \"labels\": {\n      \"app\": \"nginx-sts\"\n    },\n    \"name\": \"nginx-sts\",\n    \"namespace\": \"default\",\n    \"resourceVersion\": \"82973198\",\n    \"selfLink\": \"/apis/apps/v1/namespaces/default/statefulsets/nginx-sts\",\n    \"uid\": \"e87310a8-1387-11ea-aa02-42010a800053\"\n  },\n  \"spec\": {\n    \"podManagementPolicy\": \"OrderedReady\",\n    \"replicas\": 4,\n    \"revisionHistoryLimit\": 10,\n    \"selector\": {\n      \"matchLabels\": {\n        \"app\": \"nginx-sts\"\n      }\n    },\n    \"serviceName\": \"nginx-sts\",\n    \"template\": {\n      \"metadata\": {\n        \"annotations\": {\n          \"kubectl.kubernetes.io/restartedAt\": \"2019-12-01T13:50:44-07:00\"\n        },\n        \"creationTimestamp\": null,\n        \"labels\": {\n          \"app\": \"nginx-sts\"\n        }\n      },\n      \"spec\": {\n        \"containers\": [\n          {\n            \"image\": \"k8s.gcr.io/nginx-slim:0.8\",\n            \"imagePullPolicy\": \"IfNotPresent\",\n            \"name\": \"nginx\",\n            \"ports\": [\n              {\n                \"containerPort\": 80,\n                \"name\": \"web\",\n                \"protocol\": \"TCP\"\n              }\n            ],\n            \"resources\": {},\n            \"terminationMessagePath\": \"/dev/termination-log\",\n            \"terminationMessagePolicy\": \"File\",\n            \"volumeMounts\": [\n              {\n                \"mountPath\": \"/usr/share/nginx/html\",\n                \"name\": \"www\"\n              }\n            ]\n          }\n        ],\n        \"dnsPolicy\": \"ClusterFirst\",\n        \"restartPolicy\": \"Always\",\n        \"schedulerName\": \"default-scheduler\",\n        \"securityContext\": {},\n        \"terminationGracePeriodSeconds\": 30\n      }\n    },\n    \"updateStrategy\": {\n      \"rollingUpdate\": {\n        \"partition\": 0\n      },\n      \"type\": \"RollingUpdate\"\n    },\n    \"volumeClaimTemplates\": [\n      {\n        \"metadata\": {\n          \"creationTimestamp\": null,\n          \"name\": \"www\"\n        },\n        \"spec\": {\n          \"accessModes\": [\n            \"ReadWriteOnce\"\n          ],\n          \"dataSource\": null,\n          \"resources\": {\n            \"requests\": {\n              \"storage\": \"1Mi\"\n            }\n          },\n          \"volumeMode\": \"Filesystem\"\n        },\n        \"status\": {\n          \"phase\": \"Pending\"\n        }\n      }\n    ]\n  },\n  \"status\": {\n    \"collisionCount\": 0,\n    \"currentReplicas\": 4,\n    \"currentRevision\": \"nginx-sts-5b89ffb894\",\n    \"observedGeneration\": 5,\n    \"readyReplicas\": 4,\n    \"replicas\": 4,\n    \"updateRevision\": \"nginx-sts-5b89ffb894\",\n    \"updatedReplicas\": 4\n  }\n}"
  },
  {
    "path": "internal/render/testdata/svc.json",
    "content": "{\n  \"apiVersion\": \"v1\",\n  \"kind\": \"Service\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"v1\\\",\\\"kind\\\":\\\"Service\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"dictionary1\\\",\\\"namespace\\\":\\\"default\\\"},\\\"spec\\\":{\\\"ports\\\":[{\\\"name\\\":\\\"http\\\",\\\"port\\\":4001,\\\"targetPort\\\":\\\"http\\\"}],\\\"selector\\\":{\\\"app\\\":\\\"dictionary1\\\"},\\\"type\\\":\\\"ClusterIP\\\"}}\\n\"\n    },\n    \"creationTimestamp\": \"2019-07-10T23:10:43Z\",\n    \"name\": \"dictionary1\",\n    \"namespace\": \"default\",\n    \"resourceVersion\": \"36257616\",\n    \"selfLink\": \"/api/v1/namespaces/default/services/dictionary1\",\n    \"uid\": \"f1007a5c-a367-11e9-990f-42010a800218\"\n  },\n  \"spec\": {\n    \"clusterIP\": \"10.47.248.116\",\n    \"ports\": [\n      {\n        \"name\": \"http\",\n        \"port\": 4001,\n        \"protocol\": \"TCP\",\n        \"targetPort\": \"http\"\n      }\n    ],\n    \"selector\": {\n      \"app\": \"dictionary1\"\n    },\n    \"sessionAffinity\": \"None\",\n    \"type\": \"ClusterIP\"\n  },\n  \"status\": {\n    \"loadBalancer\": {}\n  }\n}"
  },
  {
    "path": "internal/render/types.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nconst (\n\t// NonResource represents a custom resource.\n\tNonResource = \"*\"\n)\n\nconst (\n\t// Terminating represents a pod terminating status.\n\tTerminating = \"Terminating\"\n\n\t// Running represents a pod running status.\n\tRunning = \"Running\"\n\n\t// Initialized represents a pod initialized status.\n\tInitialized = \"Initialized\"\n\n\t// Completed represents a pod completed status.\n\tCompleted = \"Completed\"\n\n\t// ContainerCreating represents a pod container status.\n\tContainerCreating = \"ContainerCreating\"\n\n\t// PodInitializing represents a pod initializing status.\n\tPodInitializing = \"PodInitializing\"\n\n\t// Pending represents a pod pending status.\n\tPending = \"Pending\"\n\n\t// Blank represents no value.\n\tBlank = \"\"\n)\n\nconst (\n\t// MissingValue indicates an unset value.\n\tMissingValue = \"<none>\"\n\n\t// NAValue indicates a value that does not pertain.\n\tNAValue = \"n/a\"\n\n\t// UnknownValue represents an unknown.\n\tUnknownValue = \"<unknown>\"\n\n\t// UnsetValue represent an unset value.\n\tUnsetValue = \"<unset>\"\n\n\t// ZeroValue represents a zero value.\n\tZeroValue = \"0\"\n)\n"
  },
  {
    "path": "internal/render/workload.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage render\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/tcell/v2\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n)\n\nvar defaultWKHeader = model1.Header{\n\tmodel1.HeaderColumn{Name: \"KIND\"},\n\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\tmodel1.HeaderColumn{Name: \"NAME\"},\n\tmodel1.HeaderColumn{Name: \"STATUS\"},\n\tmodel1.HeaderColumn{Name: \"READY\"},\n\tmodel1.HeaderColumn{Name: \"VALID\", Attrs: model1.Attrs{Wide: true}},\n\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n}\n\n// Workload renders a workload to screen.\ntype Workload struct {\n\tBase\n}\n\n// ColorerFunc colors a resource row.\nfunc (Workload) ColorerFunc() model1.ColorerFunc {\n\treturn func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color {\n\t\tc := model1.DefaultColorer(ns, h, re)\n\n\t\tidx, ok := h.IndexOf(\"STATUS\", true)\n\t\tif !ok {\n\t\t\treturn c\n\t\t}\n\t\tstatus := strings.TrimSpace(re.Row.Fields[idx])\n\t\tif status == \"DEGRADED\" {\n\t\t\tc = model1.PendingColor\n\t\t}\n\n\t\treturn c\n\t}\n}\n\n// Header returns a header rbw.\nfunc (Workload) Header(string) model1.Header {\n\treturn defaultWKHeader\n}\n\n// Render renders a K8s resource to screen.\nfunc (Workload) Render(o any, _ string, r *model1.Row) error {\n\tres, ok := o.(*WorkloadRes)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected WorkloadRes but got %T\", o)\n\t}\n\n\tr.ID = fmt.Sprintf(\"%s|%s|%s\", res.Row.Cells[0].(string), res.Row.Cells[1].(string), res.Row.Cells[2].(string))\n\tr.Fields = model1.Fields{\n\t\tres.Row.Cells[0].(string),\n\t\tres.Row.Cells[1].(string),\n\t\tres.Row.Cells[2].(string),\n\t\tres.Row.Cells[3].(string),\n\t\tres.Row.Cells[4].(string),\n\t\tres.Row.Cells[5].(string),\n\t\tToAge(res.Row.Cells[6].(metav1.Time)),\n\t}\n\n\treturn nil\n}\n\ntype WorkloadRes struct {\n\tRow metav1.TableRow\n}\n\n// GetObjectKind returns a schema object.\nfunc (*WorkloadRes) GetObjectKind() schema.ObjectKind {\n\treturn nil\n}\n\n// DeepCopyObject returns a container copy.\nfunc (a *WorkloadRes) DeepCopyObject() runtime.Object {\n\treturn a\n}\n"
  },
  {
    "path": "internal/slogs/child.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage slogs\n\nimport \"log/slog\"\n\n// CLog returns a child logger.\nfunc CLog(subsys string) *slog.Logger {\n\treturn slog.With(Subsys, subsys)\n}\n"
  },
  {
    "path": "internal/slogs/keys.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage slogs\n\nconst (\n\t// Error tracks an error logger key.\n\tError = \"error\"\n\n\t// Stack tracks a stack logger key.\n\tStack = \"stack\"\n\n\t// Subsys tracks a subsystem logger key.\n\tSubsys = \"subsys\"\n\n\t// SchemaFile tracks a schema file logger key.\n\tSchemaFile = \"schema-file\"\n\n\t// RefType tracks a reference type.\n\tRefType = \"ref-type\"\n\n\t// GVR tracks a group version resource logger key.\n\tGVR = \"gvr\"\n\n\t// AuthorSpec tracks an author spec logger key.\n\tAuthSpec = \"auth-spec\"\n\n\t// AuthStatus tracks an auth status logger key.\n\tAuthStatus = \"auth-status\"\n\n\t// AuthReason tracks an auth reason logger key.\n\tAuthReason = \"auth-reason\"\n\n\t// Ports tracks a ports logger key.\n\tPort = \"port\"\n\n\t// Address tracks an address logger key.\n\tAddress = \"address\"\n\n\t// ResName tracks a resource name logger key.\n\tResName = \"res-name\"\n\n\t// Verb tracks a verb logger key.\n\tVerb = \"verb\"\n\n\t// ResType tracks a resource type logger key.\n\tResType = \"res-type\"\n\n\t// View tracks a view logger key.\n\tView = \"view\"\n\n\t// GOR tracks a gor logger key.\n\tGOR = \"gor\"\n\n\t// Shortcut tracks a shortcut logger key.\n\tShortcut = \"shortcut\"\n\n\t// Page tracks a page logger key.\n\tPage = \"page\"\n\n\t// Skin tracks a skin logger key.\n\tSkin = \"skin\"\n\n\t// CmdHist tracks a command history logger key.\n\tCmdHist = \"cmd-hist\"\n\n\t// Image tracks an image logger key.\n\tImage = \"image\"\n\n\t// FQN tracks a fully qualified name logger key.\n\tFQN = \"fqn\"\n\n\t// ConfigName tracks a config name logger key.\n\tConfigName = \"config-name\"\n\n\t// CompName tracks a component name logger key.\n\tCompName = \"comp-name\"\n\n\t// Command tracks a command logger key.\n\tCommand = \"cmd\"\n\n\t// Context tracks a context logger key.\n\tContext = \"context\"\n\n\t// Cluster tracks a cluster logger key.\n\tCluster = \"cluster\"\n\n\t// Container tracks a container logger key.\n\tContainer = \"container\"\n\n\t// Options tracks an options logger key.\n\tOptions = \"options\"\n\n\t// Count tracks a count logger key.\n\tCount = \"count\"\n\n\t// MaxRetries tracks a max retries logger key.\n\tMaxRetries = \"max-retries\"\n\n\t// Retry tracks a retry logger key.\n\tRetry = \"retry\"\n\n\t// Message tracks a message logger key.\n\tMessage = \"message\"\n\n\t// Index tracks an index logger key.\n\tIndex = \"index\"\n\n\t// Path tracks a path logger key.\n\tPath = \"path\"\n\n\t// Dir tracks a directory logger key.\n\tDir = \"dir\"\n\n\t// FileName tracks a file name logger key.\n\tFileName = \"file-name\"\n\n\t// Key tracks a key logger key.\n\tKey = \"key\"\n\n\t// Plugin tracks a plugin logger key.\n\tPlugin = \"plugin\"\n\n\t// Component tracks a component logger key.\n\tComponent = \"component\"\n\n\t// RowID tracks a row id logger key.\n\tRowID = \"row-id\"\n\n\t// Cell tracks a cell logger key.\n\tCell = \"cell\"\n\n\t// HeaderSize tracks a header size logger key.\n\tHeaderSize = \"row-size\"\n\n\t// Namespace tracks a namespace logger key.\n\tNamespace = \"ns\"\n\n\t// AllNS tracks all namespaces logger key.\n\tAllNS = \"all-ns\"\n\n\t// Max tracks a max logger key.\n\tMax = \"max\"\n\n\t// Elapsed tracks an elapsed logger key.\n\tElapsed = \"elapsed\"\n\n\t// Log tracks a log logger key.\n\tLog = \"log\"\n\n\t// Annotation tracks an annotation logger key.\n\tAnnotation = \"annotation\"\n\n\t// Bool tracks a boolean logger key.\n\tBool = \"bool\"\n\n\t// Replicas tracks a replicas logger key.\n\tReplicas = \"replicas\"\n\n\t// Revision tracks a revision logger key.\n\tRevision = \"revision\"\n\n\t// ColName tracks a column name logger key.\n\tColName = \"col-name\"\n\n\t// URL tracks a URL logger key.\n\tURL = \"url\"\n\n\t// Attr tracks an attribute logger key.\n\tAttr = \"attr\"\n\n\t// Name tracks a name logger key.\n\tName = \"name\"\n\n\t// Matches tracks a matches logger key.\n\tMatches = \"matches\"\n\n\t// Line tracks a line logger key.\n\tLine = \"line\"\n\n\t// Sig tracks a signal logger key.\n\tSig = \"signal\"\n\n\t// Bin tracks a binary logger key.\n\tBin = \"binary\"\n\n\t// Args tracks an arguments logger key.\n\tArgs = \"args\"\n\n\t// PodPhase tracks a pod phase logger key.\n\tPodPhase = \"pod-phase\"\n\n\t// ShellPodCfg tracks a shell pod config logger key.\n\tShellPodCfg = \"shell-pod-cfg\"\n\n\t// PFID tracks a port forward id logger key.\n\tPFID = \"port-fwd-id\"\n\n\t// PFTunnel tracks a port forward tunnel logger key.\n\tPFTunnel = \"port-fwd-tunnel\"\n\n\t// Config tracks a config logger key.\n\tConfig = \"config\"\n\n\t// ResKind tracks a resource kind logger key.\n\tResKind = \"res-kind\"\n\n\t// ResGrpVersion tracks a resource group version logger key.\n\tResGrpVersion = \"res-grp-version\"\n\n\t// ID tracks an id logger key.\n\tID = \"id\"\n\n\t// ViewSetting tracks a view setting logger key.\n\tViewSetting = \"view-setting\"\n\n\t// JQExp tracks a jq expression logger key.\n\tJQExp = \"jq-exp\"\n\n\t// Duration tracks a duration logger key.\n\tDuration = \"duration\"\n\n\t// Type tracks a type logger key.\n\tType = \"type\"\n\n\t// Requested tracks a requested value logger key.\n\tRequested = \"requested\"\n\n\t// Minimum tracks a minimum value logger key.\n\tMinimum = \"minimum\"\n)\n"
  },
  {
    "path": "internal/tchart/component.go",
    "content": "package tchart\n\nimport (\n\t\"image\"\n\t\"sync\"\n\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n)\n\n// Component represents a graphic component.\ntype Component struct {\n\t*tview.Box\n\n\tbgColor, noColor           tcell.Color\n\tfocusFgColor, focusBgColor string\n\tseriesColors               []tcell.Color\n\tdimmed                     tcell.Style\n\tid, legend                 string\n\tblur                       func(tcell.Key)\n\tmx                         sync.RWMutex\n}\n\n// NewComponent returns a new component.\nfunc NewComponent(id string) *Component {\n\treturn &Component{\n\t\tBox:     tview.NewBox(),\n\t\tid:      id,\n\t\tnoColor: tcell.ColorDefault,\n\t\tseriesColors: []tcell.Color{\n\t\t\ttcell.ColorGreen,\n\t\t\ttcell.ColorOrange,\n\t\t\ttcell.ColorOrangeRed,\n\t\t},\n\t\tdimmed: tcell.StyleDefault.Background(tview.Styles.PrimitiveBackgroundColor).Foreground(tcell.ColorGray).Dim(true),\n\t}\n}\n\n// SetFocusColorNames sets the focus color names.\nfunc (c *Component) SetFocusColorNames(fg, bg string) {\n\tc.focusFgColor, c.focusBgColor = fg, bg\n}\n\n// SetBackgroundColor sets the graph bg color.\nfunc (c *Component) SetBackgroundColor(color tcell.Color) {\n\tc.mx.Lock()\n\tdefer c.mx.Unlock()\n\tc.Box.SetBackgroundColor(color)\n\tc.bgColor = color\n\tc.dimmed = c.dimmed.Background(color)\n}\n\n// ID returns the component ID.\nfunc (c *Component) ID() string {\n\treturn c.id\n}\n\n// SetLegend sets the component legend.\nfunc (c *Component) SetLegend(l string) {\n\tc.mx.Lock()\n\tdefer c.mx.Unlock()\n\tc.legend = l\n}\n\n// InputHandler returns the handler for this primitive.\nfunc (c *Component) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {\n\treturn c.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {\n\t\tswitch key := event.Key(); key {\n\t\tcase tcell.KeyEnter:\n\t\tcase tcell.KeyBacktab, tcell.KeyTab:\n\t\t\tif c.blur != nil {\n\t\t\t\tc.blur(key)\n\t\t\t}\n\t\t\tsetFocus(c)\n\t\t}\n\t})\n}\n\n// IsDial returns true if chart is a dial\nfunc (*Component) IsDial() bool {\n\treturn false\n}\n\n// SetBlurFunc sets a callback fn when component gets out of focus.\nfunc (c *Component) SetBlurFunc(handler func(key tcell.Key)) *Component {\n\tc.blur = handler\n\treturn c\n}\n\n// SetSeriesColors sets the component series colors.\nfunc (c *Component) SetSeriesColors(cc ...tcell.Color) {\n\tc.mx.Lock()\n\tdefer c.mx.Unlock()\n\tc.seriesColors = cc\n}\n\n// GetSeriesColorNames returns series colors by name.\nfunc (c *Component) GetSeriesColorNames() []string {\n\tc.mx.RLock()\n\tdefer c.mx.RUnlock()\n\n\tif len(c.seriesColors) < 3 {\n\t\treturn []string{\"green\", \"orange\", \"red\"}\n\t}\n\tnn := make([]string, 0, len(c.seriesColors))\n\tfor _, color := range c.seriesColors {\n\t\tfor name, co := range tcell.ColorNames {\n\t\t\tif co == color {\n\t\t\t\tnn = append(nn, name)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nn\n}\n\nfunc (c *Component) colorForSeries() []tcell.Color {\n\tc.mx.RLock()\n\tdefer c.mx.RUnlock()\n\treturn c.seriesColors\n}\n\nfunc (c *Component) asRect() image.Rectangle {\n\tx, y, width, height := c.GetInnerRect()\n\treturn image.Rectangle{\n\t\tMin: image.Point{X: x, Y: y},\n\t\tMax: image.Point{X: x + width, Y: y + height},\n\t}\n}\n"
  },
  {
    "path": "internal/tchart/component_int_test.go",
    "content": "package tchart\n\nimport (\n\t\"image\"\n\t\"testing\"\n\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestComponentAsRect(t *testing.T) {\n\tc := NewComponent(\"fred\")\n\tr := image.Rectangle{Min: image.Point{X: 0, Y: 0}, Max: image.Point{X: 15, Y: 10}}\n\n\tassert.Equal(t, r, c.asRect())\n}\n\nfunc TestComponentColorForSeries(t *testing.T) {\n\tc := NewComponent(\"fred\")\n\tcc := c.colorForSeries()\n\n\tassert.Len(t, cc, 3)\n\tassert.Equal(t, tcell.ColorGreen, cc[0])\n\tassert.Equal(t, tcell.ColorOrange, cc[1])\n\tassert.Equal(t, tcell.ColorOrangeRed, cc[2])\n\tassert.Equal(t, []string{\"green\", \"orange\", \"orangered\"}, c.GetSeriesColorNames())\n}\n"
  },
  {
    "path": "internal/tchart/component_test.go",
    "content": "package tchart_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/tchart\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCoSeriesColorNames(t *testing.T) {\n\tc := tchart.NewComponent(\"fred\")\n\n\tc.SetSeriesColors(tcell.ColorGreen, tcell.ColorBlue, tcell.ColorRed)\n\n\tassert.Equal(t, []string{\"green\", \"blue\", \"red\"}, c.GetSeriesColorNames())\n}\n"
  },
  {
    "path": "internal/tchart/dot_matrix.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage tchart\n\nimport (\n\t\"github.com/derailed/tview\"\n)\n\nvar dots = []rune{' ', '⠂', '▤', '▥'}\n\nconst (\n\tb    = ' '\n\th    = tview.BoxDrawingsHeavyHorizontal\n\tv    = tview.BoxDrawingsHeavyVertical\n\ttl   = tview.BoxDrawingsHeavyDownAndRight\n\ttr   = tview.BoxDrawingsHeavyDownAndLeft\n\tbl   = tview.BoxDrawingsHeavyUpAndRight\n\tbr   = tview.BoxDrawingsHeavyUpAndLeft\n\tteeL = tview.BoxDrawingsHeavyVerticalAndLeft\n\tteeR = tview.BoxDrawingsHeavyVerticalAndRight\n\tlh   = '\\u2578'\n\trh   = '\\u257a'\n\thv   = '\\u2579'\n\tlv   = '\\u257b'\n)\n\n// Matrix represents a number dial.\ntype Matrix [][]rune\n\n// Orientation tracks char orientations.\ntype Orientation int\n\n// DotMatrix tracks a char matrix.\ntype DotMatrix struct {\n\trow, col int\n}\n\n// NewDotMatrix returns a new matrix.\nfunc NewDotMatrix() DotMatrix {\n\treturn DotMatrix{\n\t\trow: 3,\n\t\tcol: 3,\n\t}\n}\n\n// Print prints the matrix.\nfunc (DotMatrix) Print(n int) Matrix {\n\treturn To3x3Char(n)\n}\n\n// To3x3Char returns 3x3 number matrix.\nfunc To3x3Char(numb int) Matrix {\n\tswitch numb {\n\tcase 1:\n\t\treturn Matrix{\n\t\t\t[]rune{b, lv, b},\n\t\t\t[]rune{b, v, b},\n\t\t\t[]rune{b, hv, b},\n\t\t}\n\tcase 2:\n\t\treturn Matrix{\n\t\t\t[]rune{rh, h, tr},\n\t\t\t[]rune{tl, h, br},\n\t\t\t[]rune{bl, h, lh},\n\t\t}\n\tcase 3:\n\t\treturn Matrix{\n\t\t\t[]rune{h, h, tr},\n\t\t\t[]rune{rh, h, teeL},\n\t\t\t[]rune{h, h, br},\n\t\t}\n\tcase 4:\n\t\treturn Matrix{\n\t\t\t[]rune{lv, b, lv},\n\t\t\t[]rune{bl, h, teeL},\n\t\t\t[]rune{b, b, hv},\n\t\t}\n\tcase 5:\n\t\treturn Matrix{\n\t\t\t[]rune{tl, h, lh},\n\t\t\t[]rune{bl, h, tr},\n\t\t\t[]rune{rh, h, br},\n\t\t}\n\tcase 6:\n\t\treturn Matrix{\n\t\t\t[]rune{tl, h, lh},\n\t\t\t[]rune{teeR, h, tr},\n\t\t\t[]rune{bl, h, br},\n\t\t}\n\tcase 7:\n\t\treturn Matrix{\n\t\t\t[]rune{h, h, tr},\n\t\t\t[]rune{b, b, v},\n\t\t\t[]rune{b, b, hv},\n\t\t}\n\tcase 8:\n\t\treturn Matrix{\n\t\t\t[]rune{tl, h, tr},\n\t\t\t[]rune{teeR, h, teeL},\n\t\t\t[]rune{bl, h, br},\n\t\t}\n\tcase 9:\n\t\treturn Matrix{\n\t\t\t[]rune{tl, h, tr},\n\t\t\t[]rune{bl, h, teeL},\n\t\t\t[]rune{rh, h, br},\n\t\t}\n\tdefault:\n\t\treturn Matrix{\n\t\t\t[]rune{tl, h, tr},\n\t\t\t[]rune{v, b, v},\n\t\t\t[]rune{bl, h, br},\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/tchart/dot_matrix_test.go",
    "content": "package tchart_test\n\nimport (\n\t\"strconv\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/tchart\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestDial3x3(t *testing.T) {\n\td := tchart.NewDotMatrix()\n\tfor n := range 2 {\n\t\ti := n\n\t\tt.Run(strconv.Itoa(n), func(t *testing.T) {\n\t\t\tassert.Equal(t, tchart.To3x3Char(i), d.Print(i))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/tchart/gauge.go",
    "content": "package tchart\n\nimport (\n\t\"fmt\"\n\t\"image\"\n\t\"time\"\n\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n)\n\nconst (\n\t// DeltaSame represents no difference.\n\tDeltaSame delta = iota\n\n\t// DeltaMore represents a higher value.\n\tDeltaMore\n\n\t// DeltaLess represents a lower value.\n\tDeltaLess\n)\n\ntype State struct {\n\tOK, Fault int\n}\n\ntype delta int\n\n// Gauge represents a gauge component.\ntype Gauge struct {\n\t*Component\n\n\tstate               State\n\tresolution          int\n\tdeltaOK, deltaFault delta\n}\n\n// NewGauge returns a new gauge.\nfunc NewGauge(id string) *Gauge {\n\treturn &Gauge{\n\t\tComponent: NewComponent(id),\n\t}\n}\n\n// SetResolution overrides the default number of digits to display.\nfunc (g *Gauge) SetResolution(n int) {\n\tg.resolution = n\n}\n\n// IsDial returns true if chart is a dial\nfunc (*Gauge) IsDial() bool {\n\treturn true\n}\n\nfunc (*Gauge) SetColorIndex(int) {}\nfunc (*Gauge) SetMax(float64)    {}\nfunc (*Gauge) GetMax() float64   { return 0 }\n\n// Add adds a metric.\nfunc (*Gauge) AddMetric(time.Time, float64) {}\n\n// Add adds a new metric.\nfunc (g *Gauge) Add(ok, fault int) {\n\tg.mx.Lock()\n\tdefer g.mx.Unlock()\n\n\tg.deltaOK, g.deltaFault = computeDelta(g.state.OK, ok), computeDelta(g.state.Fault, fault)\n\tg.state = State{OK: ok, Fault: fault}\n}\n\ntype number struct {\n\tok    bool\n\tval   int\n\tstr   string\n\tdelta delta\n}\n\n// Draw draws the primitive.\nfunc (g *Gauge) Draw(sc tcell.Screen) {\n\tg.Component.Draw(sc)\n\n\tg.mx.RLock()\n\tdefer g.mx.RUnlock()\n\n\trect := g.asRect()\n\tmid := image.Point{X: rect.Min.X + rect.Dx()/2, Y: rect.Min.Y + rect.Dy()/2 - 1}\n\tvar (\n\t\tfmat = \"%d\"\n\t)\n\td1, d2 := fmt.Sprintf(fmat, g.state.OK), fmt.Sprintf(fmat, g.state.Fault)\n\n\tstyle := tcell.StyleDefault.Background(g.bgColor)\n\n\ttotal := len(d1)*3 + len(d2)*3 + 1\n\tcolors := g.colorForSeries()\n\to := image.Point{X: mid.X, Y: mid.Y - 1}\n\to.X -= total / 2\n\tg.drawNum(sc, o, number{ok: true, val: g.state.OK, delta: g.deltaOK, str: d1}, style.Foreground(colors[0]).Dim(false))\n\n\to.X, o.Y = o.X+len(d1)*3, mid.Y\n\tsc.SetContent(o.X, o.Y, '⠔', nil, style)\n\n\to.X, o.Y = o.X+1, mid.Y-1\n\tg.drawNum(sc, o, number{ok: false, val: g.state.Fault, delta: g.deltaFault, str: d2}, style.Foreground(colors[1]).Dim(false))\n\n\tif rect.Dx() > 0 && rect.Dy() > 0 && g.legend != \"\" {\n\t\tlegend := g.legend\n\t\tif g.HasFocus() {\n\t\t\tlegend = fmt.Sprintf(\"[%s:%s:]\", g.focusFgColor, g.focusBgColor) + g.legend + \"[::]\"\n\t\t}\n\t\ttview.Print(sc, legend, rect.Min.X, o.Y+3, rect.Dx(), tview.AlignCenter, tcell.ColorWhite)\n\t}\n}\n\nfunc (g *Gauge) drawNum(sc tcell.Screen, o image.Point, n number, style tcell.Style) {\n\tcolors := g.colorForSeries()\n\tif n.ok {\n\t\tstyle = style.Foreground(colors[0])\n\t\tprintDelta(sc, n.delta, o, style)\n\t}\n\n\tdm, significant := NewDotMatrix(), n.val == 0\n\tif significant {\n\t\tstyle = g.dimmed\n\t}\n\tfor i := range len(n.str) {\n\t\tif n.str[i] == '0' && !significant {\n\t\t\tg.drawDial(sc, dm.Print(int(n.str[i]-48)), o, g.dimmed)\n\t\t} else {\n\t\t\tsignificant = true\n\t\t\tg.drawDial(sc, dm.Print(int(n.str[i]-48)), o, style)\n\t\t}\n\t\to.X += 3\n\t}\n\tif !n.ok {\n\t\to.X++\n\t\tprintDelta(sc, n.delta, o, style)\n\t}\n}\n\nfunc (*Gauge) drawDial(sc tcell.Screen, m Matrix, o image.Point, style tcell.Style) {\n\tfor r := range m {\n\t\tvar c int\n\t\tfor c < len(m[r]) {\n\t\t\tdot := m[r][c]\n\t\t\tif dot != dots[0] {\n\t\t\t\tsc.SetContent(o.X+c, o.Y+r, dot, nil, style)\n\t\t\t}\n\t\t\tc++\n\t\t}\n\t}\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc computeDelta(d1, d2 int) delta {\n\tif d2 == 0 {\n\t\treturn DeltaSame\n\t}\n\n\td := d2 - d1\n\tswitch {\n\tcase d > 0:\n\t\treturn DeltaMore\n\tcase d < 0:\n\t\treturn DeltaLess\n\tdefault:\n\t\treturn DeltaSame\n\t}\n}\n\nfunc printDelta(sc tcell.Screen, d delta, o image.Point, s tcell.Style) {\n\ts = s.Dim(false)\n\tswitch d {\n\tcase DeltaLess:\n\t\tsc.SetContent(o.X-1, o.Y+1, '↓', nil, s)\n\tcase DeltaMore:\n\t\tsc.SetContent(o.X-1, o.Y+1, '↑', nil, s)\n\t}\n}\n"
  },
  {
    "path": "internal/tchart/gauge_int_test.go",
    "content": "package tchart\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestComputeDeltas(t *testing.T) {\n\tuu := map[string]struct {\n\t\td1, d2 int\n\t\te      delta\n\t}{\n\t\t\"same\": {\n\t\t\te: DeltaSame,\n\t\t},\n\t\t\"more\": {\n\t\t\td1: 10,\n\t\t\td2: 20,\n\t\t\te:  DeltaMore,\n\t\t},\n\t\t\"less\": {\n\t\t\td1: 20,\n\t\t\td2: 10,\n\t\t\te:  DeltaLess,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, computeDelta(u.d1, u.d2))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/tchart/gauge_test.go",
    "content": "package tchart_test\n\n// import (\n// \t\"testing\"\n\n// \t\"github.com/imhotepio/tchart\"\n// \t\"github.com/stretchr/testify/assert\"\n// )\n\n// func TestMetricsMaxDigits(t *testing.T) {\n// \tuu := map[string]struct {\n// \t\tm tchart.State\n// \t\te int\n// \t}{\n// \t\t\"empty\": {\n// \t\t\te: 1,\n// \t\t},\n// \t\t\"oks\": {\n// \t\t\tm: tchart.State{OK: 100, Fault: 10},\n// \t\t\te: 3,\n// \t\t},\n// \t\t\"errs\": {\n// \t\t\tm: tchart.State{OK: 10, Fault: 1000},\n// \t\t\te: 4,\n// \t\t},\n// \t}\n\n// \tfor k := range uu {\n// \t\tu := uu[k]\n// \t\tt.Run(k, func(t *testing.T) {\n// \t\t\tassert.Equal(t, u.e, u.m.MaxDigits())\n// \t\t})\n// \t}\n// }\n\n// func TestMetricsMax(t *testing.T) {\n// \tuu := map[string]struct {\n// \t\tm tchart.Metric\n// \t\te int64\n// \t}{\n// \t\t\"empty\": {\n// \t\t\te: 0,\n// \t\t},\n// \t\t\"max_ok\": {\n// \t\t\tm: tchart.Metric{S1: 100, S2: 10},\n// \t\t\te: 100,\n// \t\t},\n// \t}\n\n// \tfor k := range uu {\n// \t\tu := uu[k]\n// \t\tt.Run(k, func(t *testing.T) {\n// \t\t\tassert.Equal(t, u.e, u.m.Max())\n// \t\t})\n// \t}\n// }\n"
  },
  {
    "path": "internal/tchart/series.go",
    "content": "package tchart\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sort\"\n\t\"time\"\n)\n\ntype MetricSeries map[time.Time]float64\n\ntype Times []time.Time\n\nfunc (tt Times) Len() int {\n\treturn len(tt)\n}\n\nfunc (tt Times) Swap(i, j int) {\n\ttt[i], tt[j] = tt[j], tt[i]\n}\n\nfunc (tt Times) Less(i, j int) bool {\n\treturn tt[i].Sub(tt[j]) <= 0\n}\n\nfunc (tt Times) Includes(ti time.Time) bool {\n\tfor _, t := range tt {\n\t\tif t.Equal(ti) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (mm MetricSeries) Empty() bool {\n\treturn len(mm) == 0\n}\n\nfunc (mm MetricSeries) Merge(metrics MetricSeries) {\n\tfor k, v := range metrics {\n\t\tmm[k] = v\n\t}\n}\n\nfunc (mm MetricSeries) Dump() {\n\tslog.Debug(\"METRICS\")\n\tfor _, k := range mm.Keys() {\n\t\tslog.Debug(fmt.Sprintf(\"%v: %f\", k, mm[k]))\n\t}\n}\n\nfunc (mm MetricSeries) Add(t time.Time, f float64) {\n\tif _, ok := mm[t]; !ok {\n\t\tmm[t] = f\n\t}\n}\n\nfunc (mm MetricSeries) Keys() Times {\n\tkk := make(Times, 0, len(mm))\n\tfor k := range mm {\n\t\tkk = append(kk, k)\n\t}\n\tsort.Sort(kk)\n\n\treturn kk\n}\n\nfunc (mm MetricSeries) Truncate(size int) {\n\tkk := mm.Keys()\n\tkk = kk[0 : len(kk)-size]\n\tfor t := range mm {\n\t\tif kk.Includes(t) {\n\t\t\tcontinue\n\t\t}\n\t\tdelete(mm, kk[0])\n\t}\n}\n"
  },
  {
    "path": "internal/tchart/series_test.go",
    "content": "package tchart_test\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/tchart\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestSeriesAdd(t *testing.T) {\n\ttype tuple struct {\n\t\ttime.Time\n\t\tfloat64\n\t}\n\tuu := map[string]struct {\n\t\ttt []tuple\n\t\te  int\n\t}{\n\t\t\"one\": {\n\t\t\ttt: []tuple{\n\t\t\t\t{time.Now(), 1000},\n\t\t\t},\n\t\t\te: 6,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tss := makeSeries()\n\t\t\tfor _, tu := range u.tt {\n\t\t\t\tss.Add(tu.Time, tu.float64)\n\t\t\t}\n\t\t\tassert.Len(t, ss, u.e)\n\t\t})\n\t}\n}\n\nfunc TestSeriesTruncate(t *testing.T) {\n\tuu := map[string]struct {\n\t\tn, e int\n\t}{\n\t\t\"one\": {\n\t\t\tn: 1,\n\t\t\te: 4,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tss := makeSeries()\n\t\t\tss.Truncate(u.n)\n\t\t\tassert.Len(t, ss, u.e)\n\t\t})\n\t}\n}\n\n// Helpers...\n\nfunc makeSeries() tchart.MetricSeries {\n\treturn tchart.MetricSeries{\n\t\ttime.Now(): -100,\n\t\ttime.Now(): 0,\n\t\ttime.Now(): 100,\n\t\ttime.Now(): 50,\n\t\ttime.Now(): 10,\n\t}\n}\n"
  },
  {
    "path": "internal/tchart/sparkline.go",
    "content": "package tchart\n\nimport (\n\t\"fmt\"\n\t\"image\"\n\t\"math\"\n\t\"time\"\n\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n)\n\nvar sparks = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'}\n\nconst axisColor = \"#ff0066\"\n\ntype block struct {\n\tfull    int\n\tpartial rune\n}\n\n// SparkLine represents a sparkline component.\ntype SparkLine struct {\n\t*Component\n\n\tseries     MetricSeries\n\tmax        float64\n\tunit       string\n\tcolorIndex int\n}\n\n// NewSparkLine returns a new graph.\nfunc NewSparkLine(id, unit string) *SparkLine {\n\treturn &SparkLine{\n\t\tComponent: NewComponent(id),\n\t\tseries:    make(MetricSeries),\n\t\tunit:      unit,\n\t}\n}\n\n// GetSeriesColorNames returns series colors by name.\nfunc (s *SparkLine) GetSeriesColorNames() []string {\n\ts.mx.RLock()\n\tdefer s.mx.RUnlock()\n\n\tnn := make([]string, 0, len(s.seriesColors))\n\tfor _, color := range s.seriesColors {\n\t\tfor name, co := range tcell.ColorNames {\n\t\t\tif co == color {\n\t\t\t\tnn = append(nn, name)\n\t\t\t}\n\t\t}\n\t}\n\tif len(nn) < 3 {\n\t\tnn = append(nn, \"green\", \"orange\", \"orangered\")\n\t}\n\n\treturn nn\n}\n\nfunc (s *SparkLine) SetColorIndex(n int) {\n\ts.colorIndex = n\n}\n\nfunc (s *SparkLine) SetMax(m float64) {\n\tif m > s.max {\n\t\ts.max = m\n\t}\n}\n\nfunc (s *SparkLine) GetMax() float64 {\n\treturn s.max\n}\n\nfunc (*SparkLine) Add(int, int) {}\n\n// Add adds a metric.\nfunc (s *SparkLine) AddMetric(t time.Time, f float64) {\n\ts.mx.Lock()\n\tdefer s.mx.Unlock()\n\ts.series.Add(t, f)\n}\n\nfunc (s *SparkLine) printYAxis(screen tcell.Screen, rect image.Rectangle) {\n\tstyle := tcell.StyleDefault.Foreground(tcell.GetColor(axisColor)).Background(s.bgColor)\n\tfor y := range rect.Dy() - 3 {\n\t\tscreen.SetContent(rect.Min.X, rect.Min.Y+y, tview.BoxDrawingsLightVertical, nil, style)\n\t}\n\tscreen.SetContent(rect.Min.X, rect.Min.Y+rect.Dy()-3, tview.BoxDrawingsLightUpAndRight, nil, style)\n}\n\nfunc (s *SparkLine) printXAxis(screen tcell.Screen, rect image.Rectangle) time.Time {\n\tdx, t := rect.Dx()-1, time.Now()\n\tvals := make([]string, 0, dx)\n\tfor i := dx; i > 0; i -= 10 {\n\t\tlabel := fmt.Sprintf(\"%02d:%02d\", t.Hour(), t.Minute())\n\t\tvals = append(vals, label)\n\t\tt = t.Add(-(10 * time.Minute))\n\t}\n\n\ty := rect.Max.Y - 2\n\tfor _, v := range vals {\n\t\tif dx <= 2 {\n\t\t\tbreak\n\t\t}\n\t\ttview.Print(screen, v, rect.Min.X+dx-5, y, 6, tview.AlignCenter, tcell.ColorOrange)\n\t\tdx -= 10\n\t}\n\tstyle := tcell.StyleDefault.Foreground(tcell.GetColor(axisColor)).Background(s.bgColor)\n\tfor x := 1; x < rect.Dx()-1; x++ {\n\t\tscreen.SetContent(rect.Min.X+x, rect.Max.Y-3, tview.BoxDrawingsLightHorizontal, nil, style)\n\t}\n\n\treturn t\n}\n\n// Draw draws the graph.\nfunc (s *SparkLine) Draw(screen tcell.Screen) {\n\ts.Component.Draw(screen)\n\n\ts.mx.RLock()\n\tdefer s.mx.RUnlock()\n\n\trect := s.asRect()\n\ts.printXAxis(screen, rect)\n\n\tpadX := 1\n\ts.cutSet(rect.Dx() - padX)\n\tvar cX int\n\tif len(s.series) < rect.Dx() {\n\t\tcX = rect.Max.X - len(s.series) - 1\n\t} else {\n\t\tcX = rect.Min.X + padX\n\t}\n\n\tpad := 2\n\tif s.legend != \"\" {\n\t\tpad++\n\t}\n\tscale := float64(len(sparks)*(rect.Dy()-pad)) / float64(s.max)\n\tcolors := s.colorForSeries()\n\tcY := rect.Max.Y - pad - 1\n\tfor _, t := range s.series.Keys() {\n\t\tb := s.makeBlock(s.series[t], scale)\n\t\ts.drawBlock(rect, screen, cX, cY, b, colors[s.colorIndex%len(colors)])\n\t\tcX++\n\t}\n\n\ts.printYAxis(screen, rect)\n\n\tif rect.Dx() > 0 && rect.Dy() > 0 && s.legend != \"\" {\n\t\tlegend := s.legend\n\t\tif s.HasFocus() {\n\t\t\tlegend = fmt.Sprintf(\"[%s:%s:]\", s.focusFgColor, s.focusBgColor) + s.legend + \"[::]\"\n\t\t}\n\t\ttview.Print(screen, legend, rect.Min.X, rect.Max.Y-1, rect.Dx(), tview.AlignCenter, tcell.ColorWhite)\n\t}\n}\n\nfunc (s *SparkLine) drawBlock(r image.Rectangle, screen tcell.Screen, x, y int, b block, c tcell.Color) {\n\tstyle := tcell.StyleDefault.Foreground(c).Background(s.bgColor)\n\n\tzeroY, full := r.Min.Y, sparks[len(sparks)-1]\n\tfor range b.full {\n\t\tscreen.SetContent(x, y, full, nil, style)\n\t\ty--\n\t\tif y < zeroY {\n\t\t\tbreak\n\t\t}\n\t}\n\tif b.partial != 0 {\n\t\tscreen.SetContent(x, y, b.partial, nil, style)\n\t}\n}\n\nfunc (s *SparkLine) cutSet(width int) {\n\tif width <= 0 || s.series.Empty() {\n\t\treturn\n\t}\n\tif len(s.series) > width {\n\t\ts.series.Truncate(width)\n\t}\n}\n\nfunc (*SparkLine) makeBlock(v, scale float64) block {\n\tsc := (v * scale)\n\tscaled := math.Round(sc)\n\tp, b := int(scaled)%len(sparks), block{full: int(scaled / float64(len(sparks)))}\n\tif v < 0 {\n\t\treturn b\n\t}\n\tif p > 0 && p < len(sparks) {\n\t\tb.partial = sparks[p]\n\t}\n\n\treturn b\n}\n"
  },
  {
    "path": "internal/ui/action.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"log/slog\"\n\t\"slices\"\n\t\"sync\"\n\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\ntype (\n\t// RangeFn represents a range iteration callback.\n\tRangeFn func(tcell.Key, KeyAction)\n\n\t// ActionHandler handles a keyboard command.\n\tActionHandler func(*tcell.EventKey) *tcell.EventKey\n\n\t// ActionOpts tracks various action options.\n\tActionOpts struct {\n\t\tVisible   bool\n\t\tShared    bool\n\t\tPlugin    bool\n\t\tHotKey    bool\n\t\tDangerous bool\n\t}\n\n\t// KeyAction represents a keyboard action.\n\tKeyAction struct {\n\t\tDescription string\n\t\tAction      ActionHandler\n\t\tOpts        ActionOpts\n\t}\n\n\t// KeyMap tracks key to action mappings.\n\tKeyMap map[tcell.Key]KeyAction\n\n\t// KeyActions tracks mappings between keystrokes and actions.\n\tKeyActions struct {\n\t\tactions KeyMap\n\t\tmx      sync.RWMutex\n\t}\n)\n\n// NewKeyAction returns a new keyboard action.\nfunc NewKeyAction(d string, a ActionHandler, visible bool) KeyAction {\n\treturn NewKeyActionWithOpts(d, a, ActionOpts{\n\t\tVisible: visible,\n\t})\n}\n\n// NewSharedKeyAction returns a new shared keyboard action.\nfunc NewSharedKeyAction(d string, a ActionHandler, visible bool) KeyAction {\n\treturn NewKeyActionWithOpts(d, a, ActionOpts{\n\t\tVisible: visible,\n\t\tShared:  true,\n\t})\n}\n\n// NewKeyActionWithOpts returns a new keyboard action.\nfunc NewKeyActionWithOpts(d string, a ActionHandler, opts ActionOpts) KeyAction {\n\treturn KeyAction{\n\t\tDescription: d,\n\t\tAction:      a,\n\t\tOpts:        opts,\n\t}\n}\n\n// NewKeyActions returns a new instance.\nfunc NewKeyActions() *KeyActions {\n\treturn &KeyActions{\n\t\tactions: make(map[tcell.Key]KeyAction),\n\t}\n}\n\n// NewKeyActionsFromMap construct actions from key map.\nfunc NewKeyActionsFromMap(mm KeyMap) *KeyActions {\n\treturn &KeyActions{actions: mm}\n}\n\n// Get fetches an action given a key.\nfunc (a *KeyActions) Get(key tcell.Key) (KeyAction, bool) {\n\ta.mx.RLock()\n\tdefer a.mx.RUnlock()\n\n\tv, ok := a.actions[key]\n\n\treturn v, ok\n}\n\n// Len returns action mapping count.\nfunc (a *KeyActions) Len() int {\n\ta.mx.RLock()\n\tdefer a.mx.RUnlock()\n\n\treturn len(a.actions)\n}\n\n// Reset clears out actions.\nfunc (a *KeyActions) Reset(aa *KeyActions) {\n\ta.Clear()\n\ta.Merge(aa)\n}\n\n// Range ranges over all actions and triggers a given function.\nfunc (a *KeyActions) Range(f RangeFn) {\n\tvar km KeyMap\n\ta.mx.RLock()\n\tkm = a.actions\n\ta.mx.RUnlock()\n\n\tfor k, v := range km {\n\t\tf(k, v)\n\t}\n}\n\n// Add adds a new key action.\nfunc (a *KeyActions) Add(k tcell.Key, ka KeyAction) {\n\ta.mx.Lock()\n\tdefer a.mx.Unlock()\n\n\ta.actions[k] = ka\n}\n\n// Bulk bulk insert key mappings.\nfunc (a *KeyActions) Bulk(aa KeyMap) {\n\ta.mx.Lock()\n\tdefer a.mx.Unlock()\n\n\tfor k, v := range aa {\n\t\ta.actions[k] = v\n\t}\n}\n\n// Merge merges given actions into existing set.\nfunc (a *KeyActions) Merge(aa *KeyActions) {\n\ta.mx.Lock()\n\tdefer a.mx.Unlock()\n\n\tfor k, v := range aa.actions {\n\t\ta.actions[k] = v\n\t}\n}\n\n// Clear remove all actions.\nfunc (a *KeyActions) Clear() {\n\ta.mx.Lock()\n\tdefer a.mx.Unlock()\n\n\tfor k := range a.actions {\n\t\tdelete(a.actions, k)\n\t}\n}\n\n// ClearDanger remove all dangerous actions.\nfunc (a *KeyActions) ClearDanger() {\n\ta.mx.Lock()\n\tdefer a.mx.Unlock()\n\n\tfor k, v := range a.actions {\n\t\tif v.Opts.Dangerous {\n\t\t\tdelete(a.actions, k)\n\t\t}\n\t}\n}\n\n// Set replace actions with new ones.\nfunc (a *KeyActions) Set(aa *KeyActions) {\n\ta.mx.Lock()\n\tdefer a.mx.Unlock()\n\n\tfor k, v := range aa.actions {\n\t\ta.actions[k] = v\n\t}\n}\n\n// Delete deletes actions by the given keys.\nfunc (a *KeyActions) Delete(kk ...tcell.Key) {\n\ta.mx.Lock()\n\tdefer a.mx.Unlock()\n\n\tfor _, k := range kk {\n\t\tdelete(a.actions, k)\n\t}\n}\n\n// Hints returns a collection of hints.\nfunc (a *KeyActions) Hints() model.MenuHints {\n\ta.mx.RLock()\n\tdefer a.mx.RUnlock()\n\n\tkk := make([]tcell.Key, 0, len(a.actions))\n\tfor k := range a.actions {\n\t\tif !a.actions[k].Opts.Shared {\n\t\t\tkk = append(kk, k)\n\t\t}\n\t}\n\tslices.Sort(kk)\n\n\thh := make(model.MenuHints, 0, len(kk))\n\tfor _, k := range kk {\n\t\tif name, ok := tcell.KeyNames[k]; ok {\n\t\t\thh = append(hh,\n\t\t\t\tmodel.MenuHint{\n\t\t\t\t\tMnemonic:    name,\n\t\t\t\t\tDescription: a.actions[k].Description,\n\t\t\t\t\tVisible:     a.actions[k].Opts.Visible,\n\t\t\t\t},\n\t\t\t)\n\t\t} else {\n\t\t\tslog.Error(\"Unable to locate key name\", slogs.Key, k)\n\t\t}\n\t}\n\n\treturn hh\n}\n"
  },
  {
    "path": "internal/ui/action_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestKeyActionsHints(t *testing.T) {\n\tkk := ui.NewKeyActionsFromMap(ui.KeyMap{\n\t\tui.KeyF: ui.NewKeyAction(\"fred\", nil, true),\n\t\tui.KeyB: ui.NewKeyAction(\"blee\", nil, true),\n\t\tui.KeyZ: ui.NewKeyAction(\"zorg\", nil, false),\n\t})\n\n\thh := kk.Hints()\n\n\tassert.Len(t, hh, 3)\n\tassert.Equal(t, model.MenuHint{Mnemonic: \"b\", Description: \"blee\", Visible: true}, hh[0])\n}\n"
  },
  {
    "path": "internal/ui/app.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"log/slog\"\n\t\"os\"\n\t\"sync\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n)\n\n// App represents an application.\ntype App struct {\n\t*tview.Application\n\tConfigurator\n\n\tMain    *Pages\n\tflash   *model.Flash\n\tactions *KeyActions\n\tviews   map[string]tview.Primitive\n\tcmdBuff *model.FishBuff\n\trunning bool\n\tmx      sync.RWMutex\n}\n\n// NewApp returns a new app.\nfunc NewApp(cfg *config.Config, _ string) *App {\n\ta := App{\n\t\tApplication:  tview.NewApplication(),\n\t\tactions:      NewKeyActions(),\n\t\tConfigurator: Configurator{Config: cfg, Styles: config.NewStyles()},\n\t\tMain:         NewPages(),\n\t\tflash:        model.NewFlash(model.DefaultFlashDelay),\n\t\tcmdBuff:      model.NewFishBuff(':', model.CommandBuffer),\n\t}\n\n\ta.views = map[string]tview.Primitive{\n\t\t\"menu\":   NewMenu(a.Styles),\n\t\t\"logo\":   NewLogo(a.Styles),\n\t\t\"prompt\": NewPrompt(&a, a.Config.K9s.UI.NoIcons, a.Styles),\n\t\t\"crumbs\": NewCrumbs(a.Styles),\n\t}\n\n\treturn &a\n}\n\n// Init initializes the application.\nfunc (a *App) Init() {\n\ta.bindKeys()\n\ta.Prompt().SetModel(a.cmdBuff)\n\ta.cmdBuff.AddListener(a)\n\ta.Styles.AddListener(a)\n\n\ta.SetRoot(a.Main, true).EnableMouse(a.Config.K9s.UI.EnableMouse)\n}\n\n// QueueUpdate queues up a ui action.\nfunc (a *App) QueueUpdate(f func()) {\n\tif a.Application == nil {\n\t\treturn\n\t}\n\tgo func() {\n\t\ta.Application.QueueUpdate(f)\n\t}()\n}\n\n// QueueUpdateDraw queues up a ui action and redraw the ui.\nfunc (a *App) QueueUpdateDraw(f func()) {\n\tif a.Application == nil {\n\t\treturn\n\t}\n\tgo func() {\n\t\ta.Application.QueueUpdateDraw(f)\n\t}()\n}\n\n// IsRunning checks if app is actually running.\nfunc (a *App) IsRunning() bool {\n\ta.mx.RLock()\n\tdefer a.mx.RUnlock()\n\treturn a.running\n}\n\n// SetRunning sets the app run state.\nfunc (a *App) SetRunning(f bool) {\n\ta.mx.Lock()\n\tdefer a.mx.Unlock()\n\ta.running = f\n}\n\n// BufferCompleted indicates input was accepted.\nfunc (*App) BufferCompleted(_, _ string) {}\n\n// BufferChanged indicates the buffer was changed.\nfunc (*App) BufferChanged(_, _ string) {}\n\n// BufferActive indicates the buff activity changed.\nfunc (a *App) BufferActive(state bool, _ model.BufferKind) {\n\tflex, ok := a.Main.GetPrimitive(\"main\").(*tview.Flex)\n\tif !ok {\n\t\treturn\n\t}\n\n\tif state && flex.ItemAt(1) != a.Prompt() {\n\t\tflex.AddItemAtIndex(1, a.Prompt(), 3, 1, false)\n\t} else if !state && flex.ItemAt(1) == a.Prompt() {\n\t\tflex.RemoveItemAtIndex(1)\n\t\ta.SetFocus(flex)\n\t}\n}\n\n// SuggestionChanged notifies of update to command suggestions.\nfunc (*App) SuggestionChanged([]string) {}\n\n// StylesChanged notifies the skin changed.\nfunc (a *App) StylesChanged(s *config.Styles) {\n\ta.Main.SetBackgroundColor(s.BgColor())\n\tif f, ok := a.Main.GetPrimitive(\"main\").(*tview.Flex); ok {\n\t\tf.SetBackgroundColor(s.BgColor())\n\t\tif !a.Config.K9s.IsHeadless() {\n\t\t\tif h, ok := f.ItemAt(0).(*tview.Flex); ok {\n\t\t\t\th.SetBackgroundColor(s.BgColor())\n\t\t\t} else {\n\t\t\t\tslog.Warn(\"Header not found\", slogs.Subsys, \"styles\", slogs.Component, \"app\")\n\t\t\t}\n\t\t}\n\t} else {\n\t\tslog.Error(\"Main panel not found\", slogs.Subsys, \"styles\", slogs.Component, \"app\")\n\t}\n}\n\n// Conn returns an api server connection.\nfunc (a *App) Conn() client.Connection {\n\treturn a.Config.GetConnection()\n}\n\nfunc (a *App) bindKeys() {\n\ta.actions = NewKeyActionsFromMap(KeyMap{\n\t\tKeyColon:       NewKeyAction(\"Cmd\", a.activateCmd, false),\n\t\ttcell.KeyCtrlR: NewKeyAction(\"Redraw\", a.redrawCmd, false),\n\t\ttcell.KeyCtrlP: NewKeyAction(\"Persist\", a.saveCmd, false),\n\t\ttcell.KeyCtrlU: NewSharedKeyAction(\"Clear Filter\", a.clearCmd, false),\n\t\ttcell.KeyCtrlQ: NewSharedKeyAction(\"Clear Filter\", a.clearCmd, false),\n\t})\n}\n\n// BailOut exits the application.\nfunc (a *App) BailOut(exitCode int) {\n\tif err := a.Config.Save(true); err != nil {\n\t\tslog.Error(\"Config save failed!\", slogs.Error, err)\n\t}\n\n\ta.Stop()\n\tos.Exit(exitCode)\n}\n\n// ResetPrompt reset the prompt model and marks buffer as active.\nfunc (a *App) ResetPrompt(m PromptModel) {\n\tm.ClearText(false)\n\ta.Prompt().SetModel(m)\n\ta.SetFocus(a.Prompt())\n\tm.SetActive(true)\n}\n\n// ResetCmd clear out user command.\nfunc (a *App) ResetCmd() {\n\ta.cmdBuff.Reset()\n}\n\nfunc (a *App) saveCmd(*tcell.EventKey) *tcell.EventKey {\n\tif err := a.Config.Save(true); err != nil {\n\t\ta.Flash().Err(err)\n\t}\n\ta.Flash().Info(\"current context config saved\")\n\n\treturn nil\n}\n\n// ActivateCmd toggle command mode.\nfunc (a *App) ActivateCmd(b bool) {\n\ta.cmdBuff.SetActive(b)\n}\n\n// GetCmd retrieves user command.\nfunc (a *App) GetCmd() string {\n\treturn a.cmdBuff.GetText()\n}\n\n// CmdBuff returns the app cmd model.\nfunc (a *App) CmdBuff() *model.FishBuff {\n\treturn a.cmdBuff\n}\n\n// HasCmd check if cmd buffer is active and has a command.\nfunc (a *App) HasCmd() bool {\n\treturn a.cmdBuff.IsActive() && !a.cmdBuff.Empty()\n}\n\n// InCmdMode check if command mode is active.\nfunc (a *App) InCmdMode() bool {\n\treturn a.Prompt().InCmdMode()\n}\n\n// HasAction checks if key matches a registered binding.\nfunc (a *App) HasAction(key tcell.Key) (KeyAction, bool) {\n\treturn a.actions.Get(key)\n}\n\n// GetActions returns a collection of actions.\nfunc (a *App) GetActions() *KeyActions {\n\treturn a.actions\n}\n\n// AddActions returns the application actions.\nfunc (a *App) AddActions(aa *KeyActions) {\n\ta.actions.Merge(aa)\n}\n\n// Views return the application root views.\nfunc (a *App) Views() map[string]tview.Primitive {\n\treturn a.views\n}\n\nfunc (a *App) clearCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif !a.cmdBuff.IsActive() {\n\t\treturn evt\n\t}\n\ta.cmdBuff.ClearText(true)\n\n\treturn nil\n}\n\nfunc (a *App) activateCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif a.InCmdMode() {\n\t\treturn evt\n\t}\n\ta.ResetPrompt(a.cmdBuff)\n\ta.cmdBuff.ClearText(true)\n\n\treturn nil\n}\n\n// RedrawCmd forces a redraw.\nfunc (a *App) redrawCmd(evt *tcell.EventKey) *tcell.EventKey {\n\ta.QueueUpdateDraw(func() {})\n\treturn evt\n}\n\n// View Accessors...\n\n// Crumbs return app crumbs.\nfunc (a *App) Crumbs() *Crumbs {\n\treturn a.views[\"crumbs\"].(*Crumbs)\n}\n\n// Logo return the app logo.\nfunc (a *App) Logo() *Logo {\n\treturn a.views[\"logo\"].(*Logo)\n}\n\n// Prompt returns command prompt.\nfunc (a *App) Prompt() *Prompt {\n\treturn a.views[\"prompt\"].(*Prompt)\n}\n\n// Menu returns app menu.\nfunc (a *App) Menu() *Menu {\n\treturn a.views[\"menu\"].(*Menu)\n}\n\n// Flash returns a flash model.\nfunc (a *App) Flash() *model.Flash {\n\treturn a.flash\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\n// AsKey converts rune to keyboard key.\nfunc AsKey(evt *tcell.EventKey) tcell.Key {\n\tif evt.Key() != tcell.KeyRune {\n\t\treturn evt.Key()\n\t}\n\tkey := tcell.Key(evt.Rune())\n\tif evt.Modifiers() == tcell.ModAlt {\n\t\tkey = tcell.Key(int16(evt.Rune()) * int16(evt.Modifiers()))\n\t}\n\treturn key\n}\n"
  },
  {
    "path": "internal/ui/app_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config/mock\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestAppGetCmd(t *testing.T) {\n\ta := ui.NewApp(mock.NewMockConfig(t), \"\")\n\ta.Init()\n\ta.CmdBuff().SetText(\"blee\", \"\", true)\n\n\tassert.Equal(t, \"blee\", a.GetCmd())\n}\n\nfunc TestAppInCmdMode(t *testing.T) {\n\ta := ui.NewApp(mock.NewMockConfig(t), \"\")\n\ta.Init()\n\ta.CmdBuff().SetText(\"blee\", \"\", true)\n\tassert.False(t, a.InCmdMode())\n\n\ta.CmdBuff().SetActive(false)\n\tassert.False(t, a.InCmdMode())\n}\n\nfunc TestAppResetCmd(t *testing.T) {\n\ta := ui.NewApp(mock.NewMockConfig(t), \"\")\n\ta.Init()\n\ta.CmdBuff().SetText(\"blee\", \"\", true)\n\n\ta.ResetCmd()\n\n\tassert.Empty(t, a.CmdBuff().GetText())\n}\n\nfunc TestAppHasCmd(t *testing.T) {\n\ta := ui.NewApp(mock.NewMockConfig(t), \"\")\n\ta.Init()\n\n\ta.ActivateCmd(true)\n\tassert.False(t, a.HasCmd())\n\n\ta.CmdBuff().SetText(\"blee\", \"\", true)\n\tassert.True(t, a.InCmdMode())\n}\n\nfunc TestAppGetActions(t *testing.T) {\n\ta := ui.NewApp(mock.NewMockConfig(t), \"\")\n\ta.Init()\n\n\ta.GetActions().Add(ui.KeyZ, ui.KeyAction{Description: \"zorg\"})\n\n\tassert.Equal(t, 6, a.GetActions().Len())\n}\n\nfunc TestAppViews(t *testing.T) {\n\ta := ui.NewApp(mock.NewMockConfig(t), \"\")\n\ta.Init()\n\n\tvv := []string{\"crumbs\", \"logo\", \"prompt\", \"menu\"}\n\tfor i := range vv {\n\t\tv := vv[i]\n\t\tt.Run(v, func(t *testing.T) {\n\t\t\tassert.NotNil(t, a.Views()[v])\n\t\t})\n\t}\n\n\tassert.NotNil(t, a.Crumbs())\n\tassert.NotNil(t, a.Logo())\n\tassert.NotNil(t, a.Prompt())\n\tassert.NotNil(t, a.Menu())\n}\n"
  },
  {
    "path": "internal/ui/config.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/fsnotify/fsnotify\"\n)\n\n// Synchronizer manages ui event queue.\ntype synchronizer interface {\n\tFlash() *model.Flash\n\tLogo() *Logo\n\tUpdateClusterInfo()\n\tQueueUpdateDraw(func())\n\tQueueUpdate(func())\n}\n\n// Configurator represents an application configuration.\ntype Configurator struct {\n\tConfig     *config.Config\n\tStyles     *config.Styles\n\tcustomView *config.CustomView\n\tBenchFile  string\n\tskinFile   string\n}\n\nfunc (c *Configurator) CustomView() *config.CustomView {\n\tif c.customView == nil {\n\t\tc.customView = config.NewCustomView()\n\t}\n\n\treturn c.customView\n}\n\n// HasSkin returns true if a skin file was located.\nfunc (c *Configurator) HasSkin() bool {\n\treturn c.skinFile != \"\"\n}\n\n// CustomViewsWatcher watches for view config file changes.\nfunc (c *Configurator) CustomViewsWatcher(ctx context.Context, s synchronizer) error {\n\tw, err := fsnotify.NewWatcher()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase evt := <-w.Events:\n\t\t\t\tif evt.Name == config.AppViewsFile && evt.Op != fsnotify.Chmod {\n\t\t\t\t\ts.QueueUpdateDraw(func() {\n\t\t\t\t\t\tif err := c.RefreshCustomViews(); err != nil {\n\t\t\t\t\t\t\tslog.Warn(\"Custom views refresh failed\", slogs.Error, err)\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\tcase err := <-w.Errors:\n\t\t\t\tslog.Warn(\"CustomView watcher failed\", slogs.Error, err)\n\t\t\t\treturn\n\t\t\tcase <-ctx.Done():\n\t\t\t\tslog.Debug(\"CustomViewWatcher canceled\", slogs.FileName, config.AppViewsFile)\n\t\t\t\tif err := w.Close(); err != nil {\n\t\t\t\t\tslog.Error(\"Closing CustomView watcher\", slogs.Error, err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\tif err := w.Add(config.AppViewsFile); err != nil {\n\t\treturn err\n\t}\n\tslog.Debug(\"Loading custom views\", slogs.FileName, config.AppViewsFile)\n\n\treturn c.RefreshCustomViews()\n}\n\n// RefreshCustomViews load view configuration changes.\nfunc (c *Configurator) RefreshCustomViews() error {\n\tc.CustomView().Reset()\n\n\treturn c.CustomView().Load(config.AppViewsFile)\n}\n\n// SkinsDirWatcher watches for skin directory file changes.\nfunc (c *Configurator) SkinsDirWatcher(ctx context.Context, s synchronizer) error {\n\tif _, err := os.Stat(config.AppSkinsDir); errors.Is(err, fs.ErrNotExist) {\n\t\treturn err\n\t}\n\tw, err := fsnotify.NewWatcher()\n\tif err != nil {\n\t\treturn err\n\t}\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase evt := <-w.Events:\n\t\t\t\tif evt.Op != fsnotify.Chmod && filepath.Base(evt.Name) == filepath.Base(c.skinFile) {\n\t\t\t\t\tslog.Debug(\"Skin file changed detected\", slogs.FileName, c.skinFile)\n\t\t\t\t\ts.QueueUpdateDraw(func() {\n\t\t\t\t\t\tc.RefreshStyles(s)\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\tcase err := <-w.Errors:\n\t\t\t\tslog.Warn(\"Skin watcher failed\", slogs.Error, err)\n\t\t\t\treturn\n\t\t\tcase <-ctx.Done():\n\t\t\t\tslog.Debug(\"SkinWatcher canceled\", slogs.FileName, c.skinFile)\n\t\t\t\tif err := w.Close(); err != nil {\n\t\t\t\t\tslog.Error(\"Closing Skin watcher\", slogs.Error, err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\tslog.Debug(\"SkinWatcher initialized\", slogs.Dir, config.AppSkinsDir)\n\treturn w.Add(config.AppSkinsDir)\n}\n\n// ConfigWatcher watches for config settings changes.\nfunc (c *Configurator) ConfigWatcher(ctx context.Context, s synchronizer) error {\n\tw, err := fsnotify.NewWatcher()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase evt := <-w.Events:\n\t\t\t\tif evt.Has(fsnotify.Create) || evt.Has(fsnotify.Write) {\n\t\t\t\t\tslog.Debug(\"ConfigWatcher file changed\", slogs.FileName, evt.Name)\n\t\t\t\t\tif evt.Name == config.AppConfigFile {\n\t\t\t\t\t\tif err := c.Config.Load(evt.Name, false); err != nil {\n\t\t\t\t\t\t\tslog.Error(\"K9s config reload failed\", slogs.Error, err)\n\t\t\t\t\t\t\ts.Flash().Warn(\"k9s config reload failed. Check k9s logs!\")\n\t\t\t\t\t\t\ts.Logo().Warn(\"K9s config reload failed!\")\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif err := c.Config.K9s.Reload(); err != nil {\n\t\t\t\t\t\t\tslog.Error(\"K9s context config reload failed\", slogs.Error, err)\n\t\t\t\t\t\t\ts.Flash().Warn(\"Context config reload failed. Check k9s logs!\")\n\t\t\t\t\t\t\ts.Logo().Warn(\"Context config reload failed!\")\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\ts.QueueUpdateDraw(func() {\n\t\t\t\t\t\tc.RefreshStyles(s)\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\tcase err := <-w.Errors:\n\t\t\t\tslog.Warn(\"ConfigWatcher failed\", slogs.Error, err)\n\t\t\t\treturn\n\t\t\tcase <-ctx.Done():\n\t\t\t\tslog.Debug(\"ConfigWatcher canceled\")\n\t\t\t\tif err := w.Close(); err != nil {\n\t\t\t\t\tslog.Error(\"Canceling ConfigWatcher\", slogs.Error, err)\n\t\t\t\t}\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\tslog.Debug(\"ConfigWatcher watching\", slogs.FileName, config.AppConfigFile)\n\tif err := w.Add(config.AppConfigFile); err != nil {\n\t\treturn err\n\t}\n\n\tcl, ct, ok := c.activeConfig()\n\tif !ok {\n\t\treturn nil\n\t}\n\tctConfigFile := config.AppContextConfig(cl, ct)\n\tslog.Debug(\"ConfigWatcher watching\", slogs.FileName, ctConfigFile)\n\n\treturn w.Add(ctConfigFile)\n}\n\nfunc (c *Configurator) activeSkin() (string, bool) {\n\tvar skin string\n\tif c.Config == nil || c.Config.K9s == nil {\n\t\treturn skin, false\n\t}\n\n\tif env_skin := os.Getenv(\"K9S_SKIN\"); env_skin != \"\" {\n\t\tif _, err := os.Stat(config.SkinFileFromName(env_skin)); err == nil {\n\t\t\tskin = env_skin\n\t\t\tslog.Debug(\"Loading env skin\", slogs.Skin, skin)\n\t\t\treturn skin, true\n\t\t}\n\t}\n\n\tif ct, err := c.Config.K9s.ActiveContext(); err == nil && ct.Skin != \"\" {\n\t\tif _, err := os.Stat(config.SkinFileFromName(ct.Skin)); err == nil {\n\t\t\tskin = ct.Skin\n\t\t\tslog.Debug(\"Loading context skin\",\n\t\t\t\tslogs.Skin, skin,\n\t\t\t\tslogs.Context, c.Config.K9s.ActiveContextName(),\n\t\t\t)\n\t\t\treturn skin, true\n\t\t}\n\t}\n\n\tif sk := c.Config.K9s.UI.Skin; sk != \"\" {\n\t\tif _, err := os.Stat(config.SkinFileFromName(sk)); err == nil {\n\t\t\tskin = sk\n\t\t\tslog.Debug(\"Loading global skin\", slogs.Skin, skin)\n\t\t\treturn skin, true\n\t\t}\n\t}\n\n\treturn skin, skin != \"\"\n}\n\nfunc (c *Configurator) activeConfig() (cluster, contxt string, ok bool) {\n\tif c.Config == nil || c.Config.K9s == nil {\n\t\treturn\n\t}\n\tct, err := c.Config.K9s.ActiveContext()\n\tif err != nil {\n\t\treturn\n\t}\n\tcluster, contxt = ct.GetClusterName(), c.Config.K9s.ActiveContextName()\n\tif cluster != \"\" && contxt != \"\" {\n\t\tok = true\n\t}\n\n\treturn\n}\n\n// RefreshStyles load for skin configuration changes.\nfunc (c *Configurator) RefreshStyles(s synchronizer) {\n\ts.UpdateClusterInfo()\n\tif c.Styles == nil {\n\t\tc.Styles = config.NewStyles()\n\t}\n\tdefer c.loadSkinFile(s)\n\n\tcl, ct, ok := c.activeConfig()\n\tif !ok {\n\t\treturn\n\t}\n\t// !!BOZO!! Lame move out!\n\tif bc, err := config.EnsureBenchmarksCfgFile(cl, ct); err != nil {\n\t\tslog.Warn(\"No benchmark config file found\",\n\t\t\tslogs.Cluster, cl,\n\t\t\tslogs.Context, ct,\n\t\t\tslogs.Error, err,\n\t\t)\n\t} else {\n\t\tc.BenchFile = bc\n\t}\n}\n\nfunc (c *Configurator) loadSkinFile(synchronizer) {\n\tinvert := c.Config.K9s.IsInvert()\n\tskin, ok := c.activeSkin()\n\tif !ok {\n\t\tslog.Debug(\"No custom skin found. Using stock skin\")\n\t\tc.updateStyles(\"\", invert)\n\t\treturn\n\t}\n\n\tskinFile := config.SkinFileFromName(skin)\n\tslog.Debug(\"Loading skin file\", slogs.Skin, skinFile)\n\tif err := c.Styles.Load(skinFile, invert); err != nil {\n\t\tif errors.Is(err, os.ErrNotExist) {\n\t\t\tslog.Warn(\"Skin file not found in skins dir\",\n\t\t\t\tslogs.Skin, filepath.Base(skinFile),\n\t\t\t\tslogs.Dir, config.AppSkinsDir,\n\t\t\t\tslogs.Error, err,\n\t\t\t)\n\t\t\tc.updateStyles(\"\", invert)\n\t\t} else {\n\t\t\tslog.Error(\"Failed to parse skin file\",\n\t\t\t\tslogs.Path, filepath.Base(skinFile),\n\t\t\t\tslogs.Error, err,\n\t\t\t)\n\t\t\tc.updateStyles(skinFile, invert)\n\t\t}\n\t} else {\n\t\tc.updateStyles(skinFile, invert)\n\t}\n}\n\nfunc (c *Configurator) updateStyles(f string, invert bool) {\n\tc.skinFile = f\n\tif f == \"\" {\n\t\tc.Styles.Reset(invert)\n\t}\n\tc.Styles.Update()\n\n\tmodel1.ModColor = c.Styles.Frame().Status.ModifyColor.Color()\n\tmodel1.AddColor = c.Styles.Frame().Status.AddColor.Color()\n\tmodel1.ErrColor = c.Styles.Frame().Status.ErrorColor.Color()\n\tmodel1.StdColor = c.Styles.Frame().Status.NewColor.Color()\n\tmodel1.PendingColor = c.Styles.Frame().Status.PendingColor.Color()\n\tmodel1.HighlightColor = c.Styles.Frame().Status.HighlightColor.Color()\n\tmodel1.KillColor = c.Styles.Frame().Status.KillColor.Color()\n\tmodel1.CompletedColor = c.Styles.Frame().Status.CompletedColor.Color()\n}\n"
  },
  {
    "path": "internal/ui/config_int_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/config/mock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/cli-runtime/pkg/genericclioptions\"\n)\n\nfunc Test_activeConfig(t *testing.T) {\n\trequire.NoError(t, os.Setenv(config.K9sEnvConfigDir, \"/tmp/test-config\"))\n\trequire.NoError(t, config.InitLocs())\n\n\tcl, ct := \"cl-1\", \"ct-1-1\"\n\tuu := map[string]struct {\n\t\tcl, ct string\n\t\tcfg    *Configurator\n\t\tok     bool\n\t}{\n\t\t\"empty\": {\n\t\t\tcfg: &Configurator{},\n\t\t},\n\n\t\t\"plain\": {\n\t\t\tcfg: &Configurator{Config: config.NewConfig(\n\t\t\t\tmock.NewMockKubeSettings(&genericclioptions.ConfigFlags{\n\t\t\t\t\tClusterName: &cl,\n\t\t\t\t\tContext:     &ct,\n\t\t\t\t}))},\n\t\t\tcl: cl,\n\t\t\tct: ct,\n\t\t\tok: true,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tcfg := u.cfg\n\t\t\tif cfg.Config != nil {\n\t\t\t\t_, err := cfg.Config.K9s.ActivateContext(ct)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t\tcl, ct, ok := cfg.activeConfig()\n\t\t\tassert.Equal(t, u.ok, ok)\n\t\t\tif ok {\n\t\t\t\tassert.Equal(t, u.cl, cl)\n\t\t\t\tassert.Equal(t, u.ct, ct)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/ui/config_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui_test\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/config/mock\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/cli-runtime/pkg/genericclioptions\"\n)\n\nfunc TestSkinnedContext(t *testing.T) {\n\trequire.NoError(t, os.Setenv(config.K9sEnvConfigDir, \"/tmp/k9s-test\"))\n\trequire.NoError(t, config.InitLocs())\n\tdefer require.NoError(t, os.RemoveAll(config.K9sEnvConfigDir))\n\n\tsf := filepath.Join(\"..\", \"config\", \"testdata\", \"skins\", \"black-and-wtf.yaml\")\n\traw, err := os.ReadFile(sf)\n\trequire.NoError(t, err)\n\ttf := filepath.Join(config.AppSkinsDir, \"black-and-wtf.yaml\")\n\trequire.NoError(t, os.WriteFile(tf, raw, data.DefaultFileMod))\n\n\tvar cfg ui.Configurator\n\tcfg.Config = mock.NewMockConfig(t)\n\tcl, ct := \"cl-1\", \"ct-1\"\n\tflags := genericclioptions.ConfigFlags{\n\t\tClusterName: &cl,\n\t\tContext:     &ct,\n\t}\n\n\tcfg.Config.K9s = config.NewK9s(\n\t\tmock.NewMockConnection(),\n\t\tmock.NewMockKubeSettings(&flags))\n\t_, err = cfg.Config.K9s.ActivateContext(\"ct-1-1\")\n\trequire.NoError(t, err)\n\tcfg.Config.K9s.UI = config.UI{Skin: \"black-and-wtf\"}\n\tcfg.RefreshStyles(newMockSynchronizer())\n\tassert.True(t, cfg.HasSkin())\n\tassert.Equal(t, tcell.ColorGhostWhite.TrueColor(), model1.StdColor)\n\tassert.Equal(t, tcell.ColorWhiteSmoke.TrueColor(), model1.ErrColor)\n}\n\nfunc TestBenchConfig(t *testing.T) {\n\trequire.NoError(t, os.Setenv(config.K9sEnvConfigDir, \"/tmp/test-config\"))\n\trequire.NoError(t, config.InitLocs())\n\tdefer require.NoError(t, os.RemoveAll(config.K9sEnvConfigDir))\n\n\tbc, err := config.EnsureBenchmarksCfgFile(\"cl-1\", \"ct-1\")\n\trequire.NoError(t, err)\n\tassert.Equal(t, \"/tmp/test-config/clusters/cl-1/ct-1/benchmarks.yaml\", bc)\n}\n\n// Helpers...\n\ntype synchronizer struct{}\n\nfunc newMockSynchronizer() synchronizer {\n\treturn synchronizer{}\n}\n\nfunc (synchronizer) Flash() *model.Flash {\n\treturn model.NewFlash(100 * time.Millisecond)\n}\nfunc (synchronizer) Logo() *ui.Logo         { return nil }\nfunc (synchronizer) UpdateClusterInfo()     {}\nfunc (synchronizer) QueueUpdateDraw(func()) {}\nfunc (synchronizer) QueueUpdate(func())     {}\n"
  },
  {
    "path": "internal/ui/crumbs.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/tview\"\n)\n\n// Crumbs represents user breadcrumbs.\ntype Crumbs struct {\n\t*tview.TextView\n\n\tstyles *config.Styles\n\tstack  *model.Stack\n}\n\n// NewCrumbs returns a new breadcrumb view.\nfunc NewCrumbs(styles *config.Styles) *Crumbs {\n\tc := Crumbs{\n\t\tstack:    model.NewStack(),\n\t\tstyles:   styles,\n\t\tTextView: tview.NewTextView(),\n\t}\n\tc.SetBackgroundColor(styles.BgColor())\n\tc.SetTextAlign(tview.AlignLeft)\n\tc.SetBorderPadding(0, 0, 1, 1)\n\tc.SetDynamicColors(true)\n\tstyles.AddListener(&c)\n\n\treturn &c\n}\n\n// StylesChanged notifies skin changed.\nfunc (c *Crumbs) StylesChanged(s *config.Styles) {\n\tc.styles = s\n\tc.SetBackgroundColor(s.BgColor())\n\tc.refresh(c.stack.Flatten())\n}\n\n// StackPushed indicates a new item was added.\nfunc (c *Crumbs) StackPushed(comp model.Component) {\n\tc.stack.Push(comp)\n\tc.refresh(c.stack.Flatten())\n}\n\n// StackPopped indicates an item was deleted.\nfunc (c *Crumbs) StackPopped(_, _ model.Component) {\n\tc.stack.Pop()\n\tc.refresh(c.stack.Flatten())\n}\n\n// StackTop indicates the top of the stack.\nfunc (*Crumbs) StackTop(model.Component) {}\n\n// Refresh updates view with new crumbs.\nfunc (c *Crumbs) refresh(crumbs []string) {\n\tc.Clear()\n\tlast, bgColor := len(crumbs)-1, c.styles.Frame().Crumb.BgColor\n\tfor i, crumb := range crumbs {\n\t\tif i == last {\n\t\t\tbgColor = c.styles.Frame().Crumb.ActiveColor\n\t\t}\n\t\t_, _ = fmt.Fprintf(c, \"[%s:%s:b] <%s> [-:%s:-] \",\n\t\t\tc.styles.Frame().Crumb.FgColor,\n\t\t\tbgColor, strings.ReplaceAll(strings.ToLower(crumb), \" \", \"\"),\n\t\t\tc.styles.Body().BgColor)\n\t}\n}\n"
  },
  {
    "path": "internal/ui/crumbs_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui_test\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n)\n\nfunc init() {\n\tslog.SetDefault(slog.New(slog.DiscardHandler))\n}\n\nfunc TestNewCrumbs(t *testing.T) {\n\tv := ui.NewCrumbs(config.NewStyles())\n\tv.StackPushed(makeComponent(\"c1\"))\n\tv.StackPushed(makeComponent(\"c2\"))\n\tv.StackPushed(makeComponent(\"c3\"))\n\n\tassert.Equal(t, \"[#000000:#00ffff:b] <c1> [-:#000000:-] [#000000:#00ffff:b] <c2> [-:#000000:-] [#000000:#ffa500:b] <c3> [-:#000000:-] \\n\", v.GetText(false))\n}\n\n// Helpers...\n\ntype c struct {\n\tname string\n}\n\nfunc makeComponent(n string) c {\n\treturn c{name: n}\n}\n\nfunc (c) SetCommand(*cmd.Interpreter)                                {}\nfunc (c) InCmdMode() bool                                            { return false }\nfunc (c) HasFocus() bool                                             { return true }\nfunc (c) Hints() model.MenuHints                                     { return nil }\nfunc (c) ExtraHints() map[string]string                              { return nil }\nfunc (c c) Name() string                                             { return c.name }\nfunc (c) Draw(tcell.Screen)                                          {}\nfunc (c) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { return nil }\nfunc (c) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) {\n\treturn nil\n}\nfunc (c) SetRect(int, int, int, int)             {}\nfunc (c) GetRect() (a, b, c, d int)              { return 0, 0, 0, 0 }\nfunc (c c) GetFocusable() tview.Focusable        { return c }\nfunc (c) Focus(func(tview.Primitive))            {}\nfunc (c) Blur()                                  {}\nfunc (c) Start()                                 {}\nfunc (c) Stop()                                  {}\nfunc (c) Init(context.Context) error             { return nil }\nfunc (c) SetFilter(string, bool)                 {}\nfunc (c) SetLabelSelector(labels.Selector, bool) {}\n"
  },
  {
    "path": "internal/ui/deltas.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"k8s.io/apimachinery/pkg/api/resource\"\n)\n\nconst (\n\t// DeltaSign signals a diff.\n\tDeltaSign = \"Δ\"\n\t// PlusSign signals inc.\n\tPlusSign = \"[red::b]↑\"\n\t// MinusSign signal dec.\n\tMinusSign = \"[green::b]↓\"\n)\n\nvar percent = regexp.MustCompile(`\\A(\\d+)%\\z`)\n\nfunc deltaNumb(o, n string) (string, bool) {\n\tvar delta string\n\n\ti, ok := numerical(o)\n\tif !ok {\n\t\treturn delta, ok\n\t}\n\n\tj, _ := numerical(n)\n\tswitch {\n\tcase i < j:\n\t\tdelta = PlusSign\n\tcase i > j:\n\t\tdelta = MinusSign\n\t}\n\n\treturn delta, ok\n}\n\nfunc deltaPerc(o, n string) (string, bool) {\n\tvar delta string\n\ti, ok := percentage(o)\n\tif !ok {\n\t\treturn delta, ok\n\t}\n\n\tj, _ := percentage(n)\n\tswitch {\n\tcase i < j:\n\t\tdelta = PlusSign\n\tcase i > j:\n\t\tdelta = MinusSign\n\t}\n\n\treturn delta, ok\n}\n\nfunc deltaQty(o, n string) (string, bool) {\n\tvar delta string\n\tq1, err := resource.ParseQuantity(o)\n\tif err != nil {\n\t\treturn delta, false\n\t}\n\n\tq2, _ := resource.ParseQuantity(n)\n\tswitch q1.Cmp(q2) {\n\tcase -1:\n\t\tdelta = PlusSign\n\tcase 1:\n\t\tdelta = MinusSign\n\t}\n\treturn delta, true\n}\n\nfunc deltaDur(o, n string) (string, bool) {\n\tvar delta string\n\td1, err := time.ParseDuration(o)\n\tif err != nil {\n\t\treturn delta, false\n\t}\n\n\td2, _ := time.ParseDuration(n)\n\tswitch {\n\tcase d2-d1 > 0:\n\t\tdelta = PlusSign\n\tcase d2-d1 < 0:\n\t\tdelta = MinusSign\n\t}\n\treturn delta, true\n}\n\n// Deltas signals diffs between 2 strings.\nfunc Deltas(o, n string) string {\n\to, n = strings.TrimSpace(o), strings.TrimSpace(n)\n\tif o == \"\" || o == render.NAValue {\n\t\treturn \"\"\n\t}\n\n\tif d, ok := deltaNumb(o, n); ok {\n\t\treturn d\n\t}\n\n\tif d, ok := deltaPerc(o, n); ok {\n\t\treturn d\n\t}\n\n\tif d, ok := deltaQty(o, n); ok {\n\t\treturn d\n\t}\n\n\tif d, ok := deltaDur(o, n); ok {\n\t\treturn d\n\t}\n\n\tswitch strings.Compare(o, n) {\n\tcase 1, -1:\n\t\treturn DeltaSign\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc percentage(s string) (int, bool) {\n\tif res := percent.FindStringSubmatch(s); len(res) == 2 {\n\t\tn, _ := strconv.Atoi(res[1])\n\t\treturn n, true\n\t}\n\n\treturn 0, false\n}\n\nfunc numerical(s string) (int, bool) {\n\tn, err := strconv.Atoi(s)\n\tif err != nil {\n\t\treturn 0, false\n\t}\n\n\treturn n, true\n}\n"
  },
  {
    "path": "internal/ui/deltas_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestDeltas(t *testing.T) {\n\tuu := []struct {\n\t\ts1, s2, e string\n\t}{\n\t\t{\"\", \"\", \"\"},\n\t\t{render.MissingValue, \"\", DeltaSign},\n\t\t{render.NAValue, \"\", \"\"},\n\t\t{\"fred\", \"fred\", \"\"},\n\t\t{\"fred\", \"blee\", DeltaSign},\n\t\t{\"1\", \"1\", \"\"},\n\t\t{\"1\", \"2\", PlusSign},\n\t\t{\"2\", \"1\", MinusSign},\n\t\t{\"2m33s\", \"2m33s\", \"\"},\n\t\t{\"2m33s\", \"1m\", MinusSign},\n\t\t{\"33s\", \"1m\", PlusSign},\n\t\t{\"10Gi\", \"10Gi\", \"\"},\n\t\t{\"10Gi\", \"20Gi\", PlusSign},\n\t\t{\"30Gi\", \"20Gi\", MinusSign},\n\t\t{\"15%\", \"15%\", \"\"},\n\t\t{\"20%\", \"40%\", PlusSign},\n\t\t{\"5%\", \"2%\", MinusSign},\n\t}\n\n\tfor _, u := range uu {\n\t\tassert.Equal(t, u.e, Deltas(u.s1, u.s2))\n\t}\n}\n"
  },
  {
    "path": "internal/ui/dialog/confirm.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dialog\n\nimport (\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tview\"\n)\n\nconst dialogKey = \"dialog\"\n\ntype confirmFunc func()\n\nfunc ShowConfirmAck(app *ui.App, pages *ui.Pages, acceptStr string, override bool, title, msg string, ack confirmFunc, cancel cancelFunc) {\n\tstyles := app.Styles.Dialog()\n\n\tf := tview.NewForm()\n\tf.SetItemPadding(0)\n\tf.SetButtonsAlign(tview.AlignCenter).\n\t\tSetButtonBackgroundColor(styles.ButtonBgColor.Color()).\n\t\tSetButtonTextColor(styles.ButtonFgColor.Color()).\n\t\tSetLabelColor(styles.LabelFgColor.Color()).\n\t\tSetFieldTextColor(styles.FieldFgColor.Color())\n\tf.AddButton(\"Cancel\", func() {\n\t\tdismissConfirm(pages)\n\t\tcancel()\n\t})\n\n\tvar accept bool\n\tif override {\n\t\tchangedFn := func(t string) {\n\t\t\taccept = (t == acceptStr)\n\t\t}\n\t\tf.AddInputField(\"Confirm:\", \"\", 30, nil, changedFn)\n\t} else {\n\t\taccept = true\n\t}\n\n\tf.AddButton(\"OK\", func() {\n\t\tif !accept {\n\t\t\treturn\n\t\t}\n\t\tack()\n\t\tdismissConfirm(pages)\n\t\tcancel()\n\t})\n\tfor i := range 2 {\n\t\tb := f.GetButton(i)\n\t\tif b == nil {\n\t\t\tcontinue\n\t\t}\n\t\tb.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color())\n\t\tb.SetLabelColorActivated(styles.ButtonFocusFgColor.Color())\n\t}\n\tf.SetFocus(0)\n\tmodal := tview.NewModalForm(\"<\"+title+\">\", f)\n\tmodal.SetText(msg)\n\tmodal.SetTextColor(styles.FgColor.Color())\n\tmodal.SetDoneFunc(func(int, string) {\n\t\tdismissConfirm(pages)\n\t\tcancel()\n\t})\n\tpages.AddPage(confirmKey, modal, false, false)\n\tpages.ShowPage(confirmKey)\n}\n\n// ShowConfirm pops a confirmation dialog.\nfunc ShowConfirm(styles *config.Dialog, pages *ui.Pages, title, msg string, ack confirmFunc, cancel cancelFunc) {\n\tf := tview.NewForm().\n\t\tSetItemPadding(0).\n\t\tSetButtonsAlign(tview.AlignCenter).\n\t\tSetButtonBackgroundColor(styles.ButtonBgColor.Color()).\n\t\tSetButtonTextColor(styles.ButtonFgColor.Color()).\n\t\tSetLabelColor(styles.LabelFgColor.Color()).\n\t\tSetFieldTextColor(styles.FieldFgColor.Color()).\n\t\tSetFieldBackgroundColor(styles.BgColor.Color())\n\tf.AddButton(\"Cancel\", func() {\n\t\tdismiss(pages)\n\t\tcancel()\n\t})\n\tf.AddButton(\"OK\", func() {\n\t\tack()\n\t\tdismiss(pages)\n\t\tcancel()\n\t})\n\tfor i := range 2 {\n\t\tif b := f.GetButton(i); b != nil {\n\t\t\tb.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color())\n\t\t\tb.SetLabelColorActivated(styles.ButtonFocusFgColor.Color())\n\t\t}\n\t}\n\tf.SetFocus(0)\n\tmodal := tview.NewModalForm(\"<\"+title+\">\", f)\n\tmodal.SetText(msg)\n\tmodal.SetTextColor(styles.FgColor.Color())\n\tmodal.SetDoneFunc(func(int, string) {\n\t\tdismiss(pages)\n\t\tcancel()\n\t})\n\tpages.AddPage(dialogKey, modal, false, false)\n\tpages.ShowPage(dialogKey)\n}\n\nfunc dismiss(pages *ui.Pages) {\n\tpages.RemovePage(dialogKey)\n}\n"
  },
  {
    "path": "internal/ui/dialog/confirm_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dialog\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tview\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestConfirmDialog(t *testing.T) {\n\ta := tview.NewApplication()\n\tp := ui.NewPages()\n\ta.SetRoot(p, false)\n\tShowConfirm(new(config.Dialog), p, \"Blee\", \"Yo\", func() {}, func() {})\n\n\td := p.GetPrimitive(dialogKey).(*tview.ModalForm)\n\tassert.NotNil(t, d)\n\n\tdismiss(p)\n\tassert.Nil(t, p.GetPrimitive(dialogKey))\n}\n"
  },
  {
    "path": "internal/ui/dialog/delete.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dialog\n\nimport (\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tview\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nconst (\n\tnoDeletePropagation   = \"None\"\n\tdefaultPropagationIdx = 0\n)\n\ntype (\n\tokFunc     func(propagation *metav1.DeletionPropagation, force bool)\n\tcancelFunc func()\n)\n\nvar propagationOptions []string = []string{\n\tstring(metav1.DeletePropagationBackground),\n\tstring(metav1.DeletePropagationForeground),\n\tstring(metav1.DeletePropagationOrphan),\n\tnoDeletePropagation,\n}\n\n// ShowDelete pops a resource deletion dialog.\nfunc ShowDelete(styles *config.Dialog, pages *ui.Pages, msg string, ok okFunc, cancel cancelFunc) {\n\tpropagation, force := \"\", false\n\tf := tview.NewForm()\n\tf.SetItemPadding(0)\n\tf.SetButtonsAlign(tview.AlignCenter).\n\t\tSetButtonBackgroundColor(styles.ButtonBgColor.Color()).\n\t\tSetButtonTextColor(styles.ButtonFgColor.Color()).\n\t\tSetLabelColor(styles.LabelFgColor.Color()).\n\t\tSetFieldTextColor(styles.FieldFgColor.Color())\n\tf.AddDropDown(\"Propagation:\", propagationOptions, defaultPropagationIdx, func(_ string, optionIndex int) {\n\t\tpropagation = propagationOptions[optionIndex]\n\t})\n\tpropField := f.GetFormItemByLabel(\"Propagation:\").(*tview.DropDown)\n\tpropField.SetListStyles(\n\t\tstyles.FgColor.Color(), styles.BgColor.Color(),\n\t\tstyles.ButtonFocusFgColor.Color(), styles.ButtonFocusBgColor.Color(),\n\t)\n\tf.AddCheckbox(\"Force:\", force, func(_ string, checked bool) {\n\t\tforce = checked\n\t})\n\tf.AddButton(\"Cancel\", func() {\n\t\tdismiss(pages)\n\t\tcancel()\n\t})\n\tf.AddButton(\"OK\", func() {\n\t\tswitch propagation {\n\t\tcase noDeletePropagation:\n\t\t\tok(nil, force)\n\t\tdefault:\n\t\t\tp := metav1.DeletionPropagation(propagation)\n\t\t\tok(&p, force)\n\t\t}\n\t\tdismiss(pages)\n\t\tcancel()\n\t})\n\tfor i := range 2 {\n\t\tb := f.GetButton(i)\n\t\tif b == nil {\n\t\t\tcontinue\n\t\t}\n\t\tb.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color())\n\t\tb.SetLabelColorActivated(styles.ButtonFocusFgColor.Color())\n\t}\n\tf.SetFocus(2)\n\n\tconfirm := tview.NewModalForm(\"<Delete>\", f)\n\tconfirm.SetText(msg)\n\tconfirm.SetDoneFunc(func(int, string) {\n\t\tdismiss(pages)\n\t\tcancel()\n\t})\n\tpages.AddPage(dialogKey, confirm, false, false)\n\tpages.ShowPage(dialogKey)\n}\n"
  },
  {
    "path": "internal/ui/dialog/delete_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dialog\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tview\"\n\t\"github.com/stretchr/testify/assert\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nfunc TestDeleteDialog(t *testing.T) {\n\tp := ui.NewPages()\n\n\tokFunc := func(p *metav1.DeletionPropagation, f bool) {\n\t\tassert.Equal(t, propagationOptions[defaultPropagationIdx], p)\n\t\tassert.True(t, f)\n\t}\n\tShowDelete(new(config.Dialog), p, \"Yo\", okFunc, func() {})\n\n\td := p.GetPrimitive(dialogKey).(*tview.ModalForm)\n\tassert.NotNil(t, d)\n\n\tdismiss(p)\n\tassert.Nil(t, p.GetPrimitive(dialogKey))\n}\n"
  },
  {
    "path": "internal/ui/dialog/error.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dialog\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n)\n\n// ShowError pops an error dialog.\nfunc ShowError(styles *config.Dialog, pages *ui.Pages, msg string) {\n\tf := tview.NewForm()\n\tf.SetItemPadding(0)\n\tf.SetButtonsAlign(tview.AlignCenter).\n\t\tSetButtonBackgroundColor(styles.ButtonBgColor.Color()).\n\t\tSetButtonTextColor(styles.ButtonFgColor.Color()).\n\t\tSetLabelColor(styles.LabelFgColor.Color()).\n\t\tSetFieldTextColor(tcell.ColorIndianRed)\n\tf.AddButton(\"Dismiss\", func() {\n\t\tdismiss(pages)\n\t})\n\tif b := f.GetButton(0); b != nil {\n\t\tb.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color())\n\t\tb.SetLabelColorActivated(styles.ButtonFocusFgColor.Color())\n\t}\n\tf.SetFocus(0)\n\tmodal := tview.NewModalForm(\"<error>\", f)\n\tmodal.SetText(cowTalk(msg))\n\tmodal.SetTextColor(tcell.ColorOrangeRed)\n\tmodal.SetDoneFunc(func(int, string) {\n\t\tdismiss(pages)\n\t})\n\tpages.AddPage(dialogKey, modal, false, false)\n\tpages.ShowPage(dialogKey)\n}\n\nfunc cowTalk(says string) string {\n\tmsg := fmt.Sprintf(\"< Ruroh? %s >\", strings.TrimSuffix(says, \"\\n\"))\n\tbuff := make([]string, 0, len(cow)+3)\n\tbuff = append(buff, msg)\n\tbuff = append(buff, cow...)\n\n\treturn strings.Join(buff, \"\\n\")\n}\n\nvar cow = []string{\n\t`\\   ^__^            `,\n\t` \\  (oo)\\_______    `,\n\t`    (__)\\       )\\/\\`,\n\t`        ||----w |   `,\n\t`        ||     ||   `,\n}\n"
  },
  {
    "path": "internal/ui/dialog/error_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dialog\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tview\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestErrorDialog(t *testing.T) {\n\tp := ui.NewPages()\n\n\tShowError(new(config.Dialog), p, \"Yo\")\n\n\td := p.GetPrimitive(dialogKey).(*tview.ModalForm)\n\tassert.NotNil(t, d)\n\tdismiss(p)\n\tassert.Nil(t, p.GetPrimitive(dialogKey))\n}\n"
  },
  {
    "path": "internal/ui/dialog/plugin_inputs.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dialog\n\nimport (\n\t\"fmt\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tview\"\n)\n\nconst pluginInputsKey = \"pluginInputs\"\n\n// PluginInputValues holds the collected input values from the dialog.\ntype PluginInputValues map[string]string\n\n// PluginInputsOkFunc is called when the user confirms the plugin inputs.\ntype PluginInputsOkFunc func(values PluginInputValues)\n\n// PluginInputsFlashFunc is called to display flash messages.\ntype PluginInputsFlashFunc func(msg string)\n\n// ShowPluginInputs pops a dialog to collect plugin input values.\nfunc ShowPluginInputs(\n\tstyles *config.Dialog,\n\tpages *ui.Pages,\n\ttitle string,\n\tinputs []config.PluginInput,\n\tflash PluginInputsFlashFunc,\n\tok PluginInputsOkFunc,\n\tcancel cancelFunc,\n) {\n\tif len(inputs) == 0 {\n\t\tok(make(PluginInputValues))\n\t\treturn\n\t}\n\n\tvalues := make(PluginInputValues)\n\n\tf := tview.NewForm()\n\tf.SetItemPadding(0)\n\tf.SetButtonsAlign(tview.AlignCenter).\n\t\tSetButtonBackgroundColor(styles.ButtonBgColor.Color()).\n\t\tSetButtonTextColor(styles.ButtonFgColor.Color()).\n\t\tSetLabelColor(styles.LabelFgColor.Color()).\n\t\tSetFieldTextColor(styles.FieldFgColor.Color())\n\n\t// Add input fields based on type\n\tfor _, input := range inputs {\n\t\tlabel := input.Name\n\t\tif input.Label != \"\" {\n\t\t\tlabel = input.Label\n\t\t}\n\t\tif input.Required {\n\t\t\tlabel += \" *\"\n\t\t}\n\t\tlabel += \":\"\n\n\t\tswitch input.Type {\n\t\tcase config.InputTypeString:\n\t\t\tvalues[input.Name] = input.Default\n\t\t\tinputName := input.Name\n\t\t\tf.AddInputField(label, input.Default, 40, nil, func(text string) {\n\t\t\t\tif strings.Contains(text, \" \") {\n\t\t\t\t\ttext = fmt.Sprintf(\"%q\", text)\n\t\t\t\t}\n\t\t\t\tvalues[inputName] = text\n\t\t\t})\n\n\t\tcase config.InputTypeNumber:\n\t\t\tvalues[input.Name] = input.Default\n\t\t\tinputName := input.Name\n\t\t\tf.AddInputField(label, input.Default, 20, func(text string, _ rune) bool {\n\t\t\t\t// Allow empty, negative sign, dot for decimals, or valid numbers\n\t\t\t\tif text == \"\" || text == \"-\" || text == \".\" || text == \"-.\" {\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t\t_, err := strconv.ParseFloat(text, 64)\n\t\t\t\treturn err == nil\n\t\t\t}, func(text string) {\n\t\t\t\tvalues[inputName] = text\n\t\t\t})\n\n\t\tcase config.InputTypeBool:\n\t\t\tdefaultChecked := input.Default == \"true\"\n\t\t\tvalues[input.Name] = input.Default\n\t\t\tinputName := input.Name\n\t\t\tf.AddCheckbox(label, defaultChecked, func(_ string, checked bool) {\n\t\t\t\tvalues[inputName] = fmt.Sprintf(\"%t\", checked)\n\t\t\t})\n\n\t\tcase config.InputTypeDropdown:\n\t\t\tif len(input.Options) > 0 {\n\t\t\t\tinputName := input.Name\n\t\t\t\t// Prepend empty option so dropdown starts unselected\n\t\t\t\toptions := append([]string{\"\"}, input.Options...)\n\t\t\t\tdefaultIndex := max(0, slices.Index(options, input.Default))\n\t\t\t\tvalues[input.Name] = options[defaultIndex]\n\t\t\t\tf.AddDropDown(label, options, defaultIndex, func(_ string, optionIndex int) {\n\t\t\t\t\tif optionIndex >= 0 && optionIndex < len(options) {\n\t\t\t\t\t\tvalues[inputName] = options[optionIndex]\n\t\t\t\t\t}\n\t\t\t\t})\n\n\t\t\t\tif dropDown := f.GetFormItemByLabel(label); dropDown != nil {\n\t\t\t\t\tif dd, ok := dropDown.(*tview.DropDown); ok {\n\t\t\t\t\t\tdd.SetListStyles(\n\t\t\t\t\t\t\tstyles.FgColor.Color(), styles.BgColor.Color(),\n\t\t\t\t\t\t\tstyles.ButtonFocusFgColor.Color(), styles.ButtonFocusBgColor.Color(),\n\t\t\t\t\t\t)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Add Cancel button\n\tf.AddButton(\"Cancel\", func() {\n\t\tdismissPluginInputs(pages)\n\t\tcancel()\n\t})\n\t// Add OK button with validation\n\tf.AddButton(\"OK\", func() {\n\t\t// Validate required fields\n\t\tvar missing []string\n\t\tfor _, input := range inputs {\n\t\t\tif input.Required {\n\t\t\t\tval := values[input.Name]\n\t\t\t\t// Bools always have a value (true/false), so skip validation for them\n\t\t\t\tif input.Type != config.InputTypeBool && val == \"\" {\n\t\t\t\t\tmissing = append(missing, input.Name)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif len(missing) > 0 {\n\t\t\tif flash != nil {\n\t\t\t\tflash(\"Required fields are missing\")\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\t// Remove optional fields with zero values\n\t\tfor _, input := range inputs {\n\t\t\tif !input.Required && input.Type != config.InputTypeBool && values[input.Name] == \"\" {\n\t\t\t\tdelete(values, input.Name)\n\t\t\t}\n\t\t}\n\n\t\tok(values)\n\t\tdismissPluginInputs(pages)\n\t\tcancel()\n\t})\n\n\t// Style buttons\n\tbuttonCount := f.GetButtonCount()\n\tfor i := range buttonCount {\n\t\tif b := f.GetButton(i); b != nil {\n\t\t\tb.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color())\n\t\t\tb.SetLabelColorActivated(styles.ButtonFocusFgColor.Color())\n\t\t}\n\t}\n\n\tf.SetFocus(0)\n\n\tmodal := tview.NewModalForm(\"<\"+title+\">\", f)\n\tmodal.SetTextColor(styles.FgColor.Color())\n\tmodal.SetDoneFunc(func(int, string) {\n\t\tdismissPluginInputs(pages)\n\t\tcancel()\n\t})\n\n\tpages.AddPage(pluginInputsKey, modal, false, false)\n\tpages.ShowPage(pluginInputsKey)\n}\n\nfunc dismissPluginInputs(pages *ui.Pages) {\n\tpages.RemovePage(pluginInputsKey)\n}\n"
  },
  {
    "path": "internal/ui/dialog/prompt.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dialog\n\nimport (\n\t\"context\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tview\"\n)\n\ntype promptAction func(ctx context.Context)\n\n// ShowPrompt pops a prompt dialog.\nfunc ShowPrompt(styles *config.Dialog, pages *ui.Pages, title, msg string, action promptAction, cancel cancelFunc) {\n\tf := tview.NewForm()\n\tf.SetItemPadding(0)\n\tf.SetButtonsAlign(tview.AlignCenter).\n\t\tSetButtonBackgroundColor(styles.ButtonBgColor.Color()).\n\t\tSetButtonTextColor(styles.ButtonFgColor.Color()).\n\t\tSetLabelColor(styles.LabelFgColor.Color()).\n\t\tSetFieldTextColor(styles.FieldFgColor.Color())\n\n\tctx, cancelCtx := context.WithCancel(context.Background())\n\n\tf.AddButton(\"Cancel\", func() {\n\t\tdismiss(pages)\n\t\tcancelCtx()\n\t\tcancel()\n\t})\n\n\tfor i := range f.GetButtonCount() {\n\t\tb := f.GetButton(i)\n\t\tif b == nil {\n\t\t\tcontinue\n\t\t}\n\t\tb.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color())\n\t\tb.SetLabelColorActivated(styles.ButtonFocusFgColor.Color())\n\t}\n\n\tf.SetFocus(0)\n\tmodal := tview.NewModalForm(\"<\"+title+\">\", f)\n\tmodal.SetText(msg)\n\tmodal.SetTextColor(styles.FgColor.Color())\n\tmodal.SetDoneFunc(func(int, string) {\n\t\tdismiss(pages)\n\t\tcancelCtx()\n\t\tcancel()\n\t})\n\n\tpages.AddPage(dialogKey, modal, false, false)\n\tpages.ShowPage(dialogKey)\n\n\tgo func() {\n\t\taction(ctx)\n\t\tdismiss(pages)\n\t}()\n}\n"
  },
  {
    "path": "internal/ui/dialog/prompt_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dialog\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestShowPrompt(t *testing.T) {\n\tt.Run(\"waiting done\", func(t *testing.T) {\n\t\ta := tview.NewApplication()\n\t\tp := ui.NewPages()\n\t\ta.SetRoot(p, false)\n\n\t\tShowPrompt(new(config.Dialog), p, \"Running\", \"Pod\", func(context.Context) {\n\t\t\ttime.Sleep(time.Millisecond)\n\t\t}, func() {\n\t\t\tt.Errorf(\"unexpected cancellations\")\n\t\t})\n\t})\n\n\tt.Run(\"canceled\", func(t *testing.T) {\n\t\ta := tview.NewApplication()\n\t\tp := ui.NewPages()\n\t\ta.SetRoot(p, false)\n\n\t\tgo ShowPrompt(new(config.Dialog), p, \"Running\", \"Pod\", func(ctx context.Context) {\n\t\t\tselect {\n\t\t\tcase <-time.After(time.Second):\n\t\t\t\tt.Errorf(\"expected cancellations\")\n\t\t\tcase <-ctx.Done():\n\t\t\t}\n\t\t}, func() {})\n\n\t\ttime.Sleep(time.Second / 2)\n\t\td := p.GetPrimitive(dialogKey).(*tview.ModalForm)\n\t\tif assert.NotNil(t, d) {\n\t\t\td.InputHandler()(tcell.NewEventKey(tcell.KeyEnter, '\\n', 0), func(tview.Primitive) {})\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/ui/dialog/restart.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dialog\n\nimport (\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tview\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\ntype RestartFn func(*metav1.PatchOptions) bool\n\ntype RestartDialogOpts struct {\n\tTitle, Message string\n\tFieldManager   string\n\tAck            RestartFn\n\tCancel         cancelFunc\n}\n\nfunc ShowRestart(styles *config.Dialog, pages *ui.Pages, opts *RestartDialogOpts) {\n\tf := tview.NewForm()\n\tf.SetItemPadding(0)\n\tf.SetButtonsAlign(tview.AlignCenter).\n\t\tSetButtonBackgroundColor(styles.ButtonBgColor.Color()).\n\t\tSetButtonTextColor(styles.ButtonFgColor.Color()).\n\t\tSetLabelColor(styles.LabelFgColor.Color()).\n\t\tSetFieldTextColor(styles.FieldFgColor.Color())\n\tf.AddButton(\"Cancel\", func() {\n\t\tdismissConfirm(pages)\n\t\topts.Cancel()\n\t})\n\n\tmodal := tview.NewModalForm(\"<\"+opts.Title+\">\", f)\n\n\targs := metav1.PatchOptions{\n\t\tFieldManager: opts.FieldManager,\n\t}\n\tf.AddInputField(\"FieldManager:\", args.FieldManager, 40, nil, func(v string) {\n\t\targs.FieldManager = v\n\t})\n\n\tf.AddButton(\"OK\", func() {\n\t\tif !opts.Ack(&args) {\n\t\t\treturn\n\t\t}\n\t\tdismissConfirm(pages)\n\t\topts.Cancel()\n\t})\n\tfor i := range 2 {\n\t\tb := f.GetButton(i)\n\t\tif b == nil {\n\t\t\tcontinue\n\t\t}\n\t\tb.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color())\n\t\tb.SetLabelColorActivated(styles.ButtonFocusFgColor.Color())\n\t}\n\tf.SetFocus(1)\n\n\tmessage := opts.Message\n\tmodal.SetText(message)\n\tmodal.SetTextColor(styles.FgColor.Color())\n\tmodal.SetDoneFunc(func(int, string) {\n\t\tdismissConfirm(pages)\n\t\topts.Cancel()\n\t})\n\tpages.AddPage(confirmKey, modal, false, false)\n\tpages.ShowPage(confirmKey)\n}\n"
  },
  {
    "path": "internal/ui/dialog/selection.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dialog\n\nimport (\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tview\"\n)\n\ntype SelectAction func(index int)\n\nfunc ShowSelection(styles *config.Dialog, pages *ui.Pages, title string, options []string, action SelectAction) {\n\tlist := tview.NewList()\n\tlist.ShowSecondaryText(false)\n\tlist.SetSelectedTextColor(styles.ButtonFocusFgColor.Color())\n\tlist.SetSelectedBackgroundColor(styles.ButtonFocusBgColor.Color())\n\n\tfor _, option := range options {\n\t\tlist.AddItem(option, \"\", 0, nil)\n\t}\n\n\tmodal := ui.NewModalList(\"<\"+title+\">\", list)\n\tmodal.SetDoneFunc(func(i int, _ string) {\n\t\tdismiss(pages)\n\t\taction(i)\n\t})\n\n\tpages.AddPage(dialogKey, modal, false, false)\n\tpages.ShowPage(dialogKey)\n}\n"
  },
  {
    "path": "internal/ui/dialog/transfer.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage dialog\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tview\"\n)\n\nconst confirmKey = \"confirm\"\n\ntype TransferFn func(TransferArgs) bool\n\ntype TransferArgs struct {\n\tFrom, To, CO         string\n\tDownload, NoPreserve bool\n\tRetries              int\n}\n\ntype TransferDialogOpts struct {\n\tContainers     []string\n\tPod            string\n\tTitle, Message string\n\tRetries        int\n\tAck            TransferFn\n\tCancel         cancelFunc\n}\n\nfunc ShowUploads(styles *config.Dialog, pages *ui.Pages, opts *TransferDialogOpts) {\n\tf := tview.NewForm()\n\tf.SetItemPadding(0)\n\tf.SetButtonsAlign(tview.AlignCenter).\n\t\tSetButtonBackgroundColor(styles.ButtonBgColor.Color()).\n\t\tSetButtonTextColor(styles.ButtonFgColor.Color()).\n\t\tSetLabelColor(styles.LabelFgColor.Color()).\n\t\tSetFieldTextColor(styles.FieldFgColor.Color())\n\tf.AddButton(\"Cancel\", func() {\n\t\tdismissConfirm(pages)\n\t\topts.Cancel()\n\t})\n\n\tmodal := tview.NewModalForm(\"<\"+opts.Title+\">\", f)\n\n\targs := TransferArgs{\n\t\tFrom:    opts.Pod,\n\t\tRetries: opts.Retries,\n\t}\n\tvar fromField, toField *tview.InputField\n\targs.Download = true\n\tf.AddCheckbox(\"Download:\", args.Download, func(_ string, flag bool) {\n\t\tif flag {\n\t\t\tmodal.SetText(strings.Replace(opts.Message, \"Upload\", \"Download\", 1))\n\t\t} else {\n\t\t\tmodal.SetText(strings.Replace(opts.Message, \"Download\", \"Upload\", 1))\n\t\t}\n\t\targs.Download = flag\n\t\targs.From, args.To = args.To, args.From\n\t\tfromField.SetText(args.From)\n\t\ttoField.SetText(args.To)\n\t})\n\n\tf.AddInputField(\"From:\", args.From, 40, nil, func(v string) {\n\t\targs.From = v\n\t})\n\tf.AddInputField(\"To:\", args.To, 40, nil, func(v string) {\n\t\targs.To = v\n\t})\n\tfromField, _ = f.GetFormItemByLabel(\"From:\").(*tview.InputField)\n\ttoField, _ = f.GetFormItemByLabel(\"To:\").(*tview.InputField)\n\n\tf.AddCheckbox(\"NoPreserve:\", args.NoPreserve, func(_ string, f bool) {\n\t\targs.NoPreserve = f\n\t})\n\tif len(opts.Containers) > 0 {\n\t\targs.CO = opts.Containers[0]\n\t}\n\tf.AddInputField(\"Container:\", args.CO, 30, nil, func(v string) {\n\t\targs.CO = v\n\t})\n\tretries := strconv.Itoa(opts.Retries)\n\tf.AddInputField(\"Retries:\", retries, 30, nil, func(v string) {\n\t\tretries = v\n\n\t\tif retriesInt, err := strconv.Atoi(retries); err == nil {\n\t\t\targs.Retries = retriesInt\n\t\t}\n\t})\n\n\tf.AddButton(\"OK\", func() {\n\t\tif !opts.Ack(args) {\n\t\t\treturn\n\t\t}\n\t\tdismissConfirm(pages)\n\t\topts.Cancel()\n\t})\n\tfor i := range 2 {\n\t\tb := f.GetButton(i)\n\t\tif b == nil {\n\t\t\tcontinue\n\t\t}\n\t\tb.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color())\n\t\tb.SetLabelColorActivated(styles.ButtonFocusFgColor.Color())\n\t}\n\tf.SetFocus(0)\n\n\tmessage := opts.Message\n\tif len(opts.Containers) > 1 {\n\t\tmessage += \"\\nAvailable Containers:\" + strings.Join(opts.Containers, \",\")\n\t}\n\tmodal.SetText(message)\n\tmodal.SetTextColor(styles.FgColor.Color())\n\tmodal.SetDoneFunc(func(int, string) {\n\t\tdismissConfirm(pages)\n\t\topts.Cancel()\n\t})\n\tpages.AddPage(confirmKey, modal, false, false)\n\tpages.ShowPage(confirmKey)\n}\n\nfunc dismissConfirm(pages *ui.Pages) {\n\tpages.RemovePage(confirmKey)\n}\n"
  },
  {
    "path": "internal/ui/flash.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n)\n\nconst (\n\temoHappy = \"😎\"\n\temoDoh   = \"😗\"\n\temoRed   = \"😡\"\n)\n\n// Flash represents a flash message indicator.\ntype Flash struct {\n\t*tview.TextView\n\n\tapp      *App\n\ttestMode bool\n}\n\n// NewFlash returns a new flash view.\nfunc NewFlash(app *App) *Flash {\n\tf := Flash{\n\t\tapp:      app,\n\t\tTextView: tview.NewTextView(),\n\t}\n\tf.SetTextColor(tcell.ColorAqua)\n\tf.SetDynamicColors(true)\n\tf.SetTextAlign(tview.AlignCenter)\n\tf.SetBorderPadding(0, 0, 1, 1)\n\tf.app.Styles.AddListener(&f)\n\n\treturn &f\n}\n\n// SetTestMode for testing ONLY!\nfunc (f *Flash) SetTestMode(b bool) {\n\tf.testMode = b\n}\n\n// StylesChanged notifies listener the skin changed.\nfunc (f *Flash) StylesChanged(s *config.Styles) {\n\tf.SetBackgroundColor(s.BgColor())\n\tf.SetTextColor(s.FgColor())\n}\n\n// Watch watches for flash changes.\nfunc (f *Flash) Watch(ctx context.Context, c model.FlashChan) {\n\tdefer slog.Debug(\"Flash Watch Canceled!\")\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase msg := <-c:\n\t\t\tf.SetMessage(msg)\n\t\t}\n\t}\n}\n\n// SetMessage sets flash message and level.\nfunc (f *Flash) SetMessage(m model.LevelMessage) {\n\tfn := func() {\n\t\tif m.Text == \"\" {\n\t\t\tf.Clear()\n\t\t\treturn\n\t\t}\n\t\tf.SetTextColor(flashColor(m.Level))\n\t\tf.SetText(f.flashEmoji(m.Level) + \" \" + m.Text)\n\t}\n\n\tif f.testMode {\n\t\tfn()\n\t} else {\n\t\tf.app.QueueUpdateDraw(fn)\n\t}\n}\n\nfunc (f *Flash) flashEmoji(l model.FlashLevel) string {\n\tif f.app.Config.K9s.UI.NoIcons {\n\t\treturn \"\"\n\t}\n\t//nolint:exhaustive\n\tswitch l {\n\tcase model.FlashWarn:\n\t\treturn emoDoh\n\tcase model.FlashErr:\n\t\treturn emoRed\n\tdefault:\n\t\treturn emoHappy\n\t}\n}\n\n// Helpers...\n\nfunc flashColor(l model.FlashLevel) tcell.Color {\n\t//nolint:exhaustive\n\tswitch l {\n\tcase model.FlashWarn:\n\t\treturn tcell.ColorOrange\n\tcase model.FlashErr:\n\t\treturn tcell.ColorOrangeRed\n\tdefault:\n\t\treturn tcell.ColorNavajoWhite\n\t}\n}\n"
  },
  {
    "path": "internal/ui/flash_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/config/mock\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestFlash(t *testing.T) {\n\tconst delay = 10 * time.Millisecond\n\tuu := map[string]struct {\n\t\tl    model.FlashLevel\n\t\ti, e string\n\t}{\n\t\t\"info\": {l: model.FlashInfo, i: \"hello\", e: \"😎 hello\\n\"},\n\t\t\"warn\": {l: model.FlashWarn, i: \"hello\", e: \"😗 hello\\n\"},\n\t\t\"err\":  {l: model.FlashErr, i: \"hello\", e: \"😡 hello\\n\"},\n\t}\n\n\ta := ui.NewApp(mock.NewMockConfig(t), \"test\")\n\tf := ui.NewFlash(a)\n\tf.SetTestMode(true)\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\tgo f.Watch(ctx, a.Flash().Channel())\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\ta.Flash().SetMessage(u.l, u.i)\n\t\t\ttime.Sleep(delay)\n\t\t\tassert.Equal(t, u.e, f.GetText(false))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/ui/indicator.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/tview\"\n)\n\n// StatusIndicator represents a status indicator when main header is collapsed.\ntype StatusIndicator struct {\n\t*tview.TextView\n\n\tapp       *App\n\tstyles    *config.Styles\n\tpermanent string\n\tcancel    context.CancelFunc\n}\n\n// NewStatusIndicator returns a new status indicator.\nfunc NewStatusIndicator(app *App, styles *config.Styles) *StatusIndicator {\n\ts := StatusIndicator{\n\t\tTextView: tview.NewTextView(),\n\t\tapp:      app,\n\t\tstyles:   styles,\n\t}\n\ts.SetTextAlign(tview.AlignCenter)\n\ts.SetTextColor(styles.FgColor())\n\ts.SetBackgroundColor(styles.BgColor())\n\ts.SetDynamicColors(true)\n\tstyles.AddListener(&s)\n\n\treturn &s\n}\n\n// StylesChanged notifies the skins changed.\nfunc (s *StatusIndicator) StylesChanged(styles *config.Styles) {\n\ts.styles = styles\n\ts.SetBackgroundColor(styles.BgColor())\n\ts.SetTextColor(styles.FgColor())\n}\n\nconst statusIndicatorFmt = \"[%s::b]K9s [%s::]%s [%s::]%s:%s:%s [%s::]%s[%s::]::[%s::]%s\"\n\n// ClusterInfoUpdated notifies the cluster meta was updated.\nfunc (s *StatusIndicator) ClusterInfoUpdated(data *model.ClusterMeta) {\n\ts.app.QueueUpdateDraw(func() {\n\t\ts.SetPermanent(fmt.Sprintf(\n\t\t\tstatusIndicatorFmt,\n\t\t\ts.styles.Body().LogoColor.String(),\n\t\t\ts.styles.K9s.Info.K9sRevColor.String(),\n\t\t\tdata.K9sVer,\n\t\t\ts.styles.K9s.Info.FgColor.String(),\n\t\t\tdata.Context,\n\t\t\tdata.Cluster,\n\t\t\tdata.K8sVer,\n\t\t\ts.styles.K9s.Info.CPUColor.String(),\n\t\t\trender.PrintPerc(data.Cpu),\n\t\t\ts.styles.Body().FgColor.String(),\n\t\t\ts.styles.K9s.Info.MEMColor.String(),\n\t\t\trender.PrintPerc(data.Mem),\n\t\t))\n\t})\n}\n\n// ClusterInfoChanged notifies the cluster meta was changed.\nfunc (s *StatusIndicator) ClusterInfoChanged(prev, cur *model.ClusterMeta) {\n\tif !s.app.IsRunning() {\n\t\treturn\n\t}\n\ts.app.QueueUpdateDraw(func() {\n\t\ts.SetPermanent(fmt.Sprintf(\n\t\t\tstatusIndicatorFmt,\n\t\t\ts.styles.Body().LogoColor.String(),\n\t\t\ts.styles.K9s.Info.K9sRevColor.String(),\n\t\t\tcur.K9sVer,\n\t\t\ts.styles.K9s.Info.FgColor.String(),\n\t\t\tcur.Context,\n\t\t\tcur.Cluster,\n\t\t\tcur.K8sVer,\n\t\t\ts.styles.K9s.Info.CPUColor.String(),\n\t\t\tAsPercDelta(prev.Cpu, cur.Cpu),\n\t\t\ts.styles.Body().FgColor.String(),\n\t\t\ts.styles.K9s.Info.MEMColor.String(),\n\t\t\tAsPercDelta(prev.Cpu, cur.Mem),\n\t\t))\n\t})\n}\n\n// SetPermanent sets permanent title to be reset to after updates.\nfunc (s *StatusIndicator) SetPermanent(info string) {\n\ts.permanent = info\n\ts.SetText(info)\n}\n\n// Reset clears out the logo view and resets colors.\nfunc (s *StatusIndicator) Reset() {\n\ts.Clear()\n\ts.SetPermanent(s.permanent)\n}\n\n// Err displays a log error state.\nfunc (s *StatusIndicator) Err(msg string) {\n\ts.update(msg, \"orangered\")\n}\n\n// Warn displays a log warning state.\nfunc (s *StatusIndicator) Warn(msg string) {\n\ts.update(msg, \"mediumvioletred\")\n}\n\n// Info displays a log info state.\nfunc (s *StatusIndicator) Info(msg string) {\n\ts.update(msg, \"lawngreen\")\n}\n\nfunc (s *StatusIndicator) update(msg, c string) {\n\ts.setText(fmt.Sprintf(\"[%s::b] <%s> \", c, msg))\n}\n\nfunc (s *StatusIndicator) setText(msg string) {\n\tif s.cancel != nil {\n\t\ts.cancel()\n\t}\n\ts.SetText(msg)\n\n\tvar ctx context.Context\n\tctx, s.cancel = context.WithCancel(context.Background())\n\tgo func(ctx context.Context) {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\tcase <-time.After(5 * time.Second):\n\t\t\ts.app.QueueUpdateDraw(func() {\n\t\t\t\ts.Reset()\n\t\t\t})\n\t\t}\n\t}(ctx)\n}\n\n// Helpers...\n\n// AsPercDelta represents a percentage with a delta indicator.\nfunc AsPercDelta(ov, nv int) string {\n\tprev, cur := render.IntToStr(ov), render.IntToStr(nv)\n\treturn cur + \"%\" + Deltas(prev, cur)\n}\n"
  },
  {
    "path": "internal/ui/indicator_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/config/mock\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestIndicatorReset(t *testing.T) {\n\ti := ui.NewStatusIndicator(ui.NewApp(mock.NewMockConfig(t), \"\"), config.NewStyles())\n\ti.SetPermanent(\"Blee\")\n\ti.Info(\"duh\")\n\ti.Reset()\n\n\tassert.Equal(t, \"Blee\\n\", i.GetText(false))\n}\n\nfunc TestIndicatorInfo(t *testing.T) {\n\ti := ui.NewStatusIndicator(ui.NewApp(mock.NewMockConfig(t), \"\"), config.NewStyles())\n\ti.Info(\"Blee\")\n\n\tassert.Equal(t, \"[lawngreen::b] <Blee> \\n\", i.GetText(false))\n}\n\nfunc TestIndicatorWarn(t *testing.T) {\n\ti := ui.NewStatusIndicator(ui.NewApp(mock.NewMockConfig(t), \"\"), config.NewStyles())\n\ti.Warn(\"Blee\")\n\n\tassert.Equal(t, \"[mediumvioletred::b] <Blee> \\n\", i.GetText(false))\n}\n\nfunc TestIndicatorErr(t *testing.T) {\n\ti := ui.NewStatusIndicator(ui.NewApp(mock.NewMockConfig(t), \"\"), config.NewStyles())\n\ti.Err(\"Blee\")\n\n\tassert.Equal(t, \"[orangered::b] <Blee> \\n\", i.GetText(false))\n}\n"
  },
  {
    "path": "internal/ui/key.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport \"github.com/derailed/tcell/v2\"\n\nfunc init() {\n\tinitKeys()\n}\n\nfunc initKeys() {\n\ttcell.KeyNames[KeyHelp] = \"?\"\n\ttcell.KeyNames[KeySlash] = \"/\"\n\ttcell.KeyNames[KeySpace] = \"space\"\n\n\tinitNumbKeys()\n\tinitStdKeys()\n\tinitShiftKeys()\n\tinitShiftNumKeys()\n}\n\n// Defines numeric keys for container actions.\nconst (\n\tKey0 tcell.Key = iota + 48\n\tKey1\n\tKey2\n\tKey3\n\tKey4\n\tKey5\n\tKey6\n\tKey7\n\tKey8\n\tKey9\n)\n\n// Defines numeric keys for container actions.\nconst (\n\tKeyShift0 tcell.Key = 41\n\tKeyShift1 tcell.Key = 33\n\tKeyShift2 tcell.Key = 64\n\tKeyShift3 tcell.Key = 35\n\tKeyShift4 tcell.Key = 36\n\tKeyShift5 tcell.Key = 37\n\tKeyShift6 tcell.Key = 94\n\tKeyShift7 tcell.Key = 38\n\tKeyShift8 tcell.Key = 42\n\tKeyShift9 tcell.Key = 40\n)\n\n// Defines char keystrokes.\nconst (\n\tKeyA tcell.Key = iota + 97\n\tKeyB\n\tKeyC\n\tKeyD\n\tKeyE\n\tKeyF\n\tKeyG\n\tKeyH\n\tKeyI\n\tKeyJ\n\tKeyK\n\tKeyL\n\tKeyM\n\tKeyN\n\tKeyO\n\tKeyP\n\tKeyQ\n\tKeyR\n\tKeyS\n\tKeyT\n\tKeyU\n\tKeyV\n\tKeyW\n\tKeyX\n\tKeyY\n\tKeyZ\n\tKeyHelp         = 63\n\tKeySlash        = 47\n\tKeyColon        = 58\n\tKeySpace        = 32\n\tKeyDash         = 45\n\tKeyLeftBracket  = 91\n\tKeyRightBracket = 93\n)\n\n// Define Shift Keys.\nconst (\n\tKeyShiftA tcell.Key = iota + 65\n\tKeyShiftB\n\tKeyShiftC\n\tKeyShiftD\n\tKeyShiftE\n\tKeyShiftF\n\tKeyShiftG\n\tKeyShiftH\n\tKeyShiftI\n\tKeyShiftJ\n\tKeyShiftK\n\tKeyShiftL\n\tKeyShiftM\n\tKeyShiftN\n\tKeyShiftO\n\tKeyShiftP\n\tKeyShiftQ\n\tKeyShiftR\n\tKeyShiftS\n\tKeyShiftT\n\tKeyShiftU\n\tKeyShiftV\n\tKeyShiftW\n\tKeyShiftX\n\tKeyShiftY\n\tKeyShiftZ\n)\n\n// NumKeys tracks number keys.\nvar NumKeys = map[int]tcell.Key{\n\t0: Key0,\n\t1: Key1,\n\t2: Key2,\n\t3: Key3,\n\t4: Key4,\n\t5: Key5,\n\t6: Key6,\n\t7: Key7,\n\t8: Key8,\n\t9: Key9,\n}\n\nfunc initNumbKeys() {\n\ttcell.KeyNames[Key0] = \"0\"\n\ttcell.KeyNames[Key1] = \"1\"\n\ttcell.KeyNames[Key2] = \"2\"\n\ttcell.KeyNames[Key3] = \"3\"\n\ttcell.KeyNames[Key4] = \"4\"\n\ttcell.KeyNames[Key5] = \"5\"\n\ttcell.KeyNames[Key6] = \"6\"\n\ttcell.KeyNames[Key7] = \"7\"\n\ttcell.KeyNames[Key8] = \"8\"\n\ttcell.KeyNames[Key9] = \"9\"\n}\n\nfunc initStdKeys() {\n\ttcell.KeyNames[KeyA] = \"a\"\n\ttcell.KeyNames[KeyB] = \"b\"\n\ttcell.KeyNames[KeyC] = \"c\"\n\ttcell.KeyNames[KeyD] = \"d\"\n\ttcell.KeyNames[KeyE] = \"e\"\n\ttcell.KeyNames[KeyF] = \"f\"\n\ttcell.KeyNames[KeyG] = \"g\"\n\ttcell.KeyNames[KeyH] = \"h\"\n\ttcell.KeyNames[KeyI] = \"i\"\n\ttcell.KeyNames[KeyJ] = \"j\"\n\ttcell.KeyNames[KeyK] = \"k\"\n\ttcell.KeyNames[KeyL] = \"l\"\n\ttcell.KeyNames[KeyM] = \"m\"\n\ttcell.KeyNames[KeyN] = \"n\"\n\ttcell.KeyNames[KeyO] = \"o\"\n\ttcell.KeyNames[KeyP] = \"p\"\n\ttcell.KeyNames[KeyQ] = \"q\"\n\ttcell.KeyNames[KeyR] = \"r\"\n\ttcell.KeyNames[KeyS] = \"s\"\n\ttcell.KeyNames[KeyT] = \"t\"\n\ttcell.KeyNames[KeyU] = \"u\"\n\ttcell.KeyNames[KeyV] = \"v\"\n\ttcell.KeyNames[KeyW] = \"w\"\n\ttcell.KeyNames[KeyX] = \"x\"\n\ttcell.KeyNames[KeyY] = \"y\"\n\ttcell.KeyNames[KeyZ] = \"z\"\n}\n\nfunc initShiftNumKeys() {\n\ttcell.KeyNames[KeyShift0] = \"Shift-0\"\n\ttcell.KeyNames[KeyShift1] = \"Shift-1\"\n\ttcell.KeyNames[KeyShift2] = \"Shift-2\"\n\ttcell.KeyNames[KeyShift3] = \"Shift-3\"\n\ttcell.KeyNames[KeyShift4] = \"Shift-4\"\n\ttcell.KeyNames[KeyShift5] = \"Shift-5\"\n\ttcell.KeyNames[KeyShift6] = \"Shift-6\"\n\ttcell.KeyNames[KeyShift7] = \"Shift-7\"\n\ttcell.KeyNames[KeyShift8] = \"Shift-8\"\n\ttcell.KeyNames[KeyShift9] = \"Shift-9\"\n}\n\nfunc initShiftKeys() {\n\ttcell.KeyNames[KeyShiftA] = \"Shift-A\"\n\ttcell.KeyNames[KeyShiftB] = \"Shift-B\"\n\ttcell.KeyNames[KeyShiftC] = \"Shift-C\"\n\ttcell.KeyNames[KeyShiftD] = \"Shift-D\"\n\ttcell.KeyNames[KeyShiftE] = \"Shift-E\"\n\ttcell.KeyNames[KeyShiftF] = \"Shift-F\"\n\ttcell.KeyNames[KeyShiftG] = \"Shift-G\"\n\ttcell.KeyNames[KeyShiftH] = \"Shift-H\"\n\ttcell.KeyNames[KeyShiftI] = \"Shift-I\"\n\ttcell.KeyNames[KeyShiftJ] = \"Shift-J\"\n\ttcell.KeyNames[KeyShiftK] = \"Shift-K\"\n\ttcell.KeyNames[KeyShiftL] = \"Shift-L\"\n\ttcell.KeyNames[KeyShiftM] = \"Shift-M\"\n\ttcell.KeyNames[KeyShiftN] = \"Shift-N\"\n\ttcell.KeyNames[KeyShiftO] = \"Shift-O\"\n\ttcell.KeyNames[KeyShiftP] = \"Shift-P\"\n\ttcell.KeyNames[KeyShiftQ] = \"Shift-Q\"\n\ttcell.KeyNames[KeyShiftR] = \"Shift-R\"\n\ttcell.KeyNames[KeyShiftS] = \"Shift-S\"\n\ttcell.KeyNames[KeyShiftT] = \"Shift-T\"\n\ttcell.KeyNames[KeyShiftU] = \"Shift-U\"\n\ttcell.KeyNames[KeyShiftV] = \"Shift-V\"\n\ttcell.KeyNames[KeyShiftW] = \"Shift-W\"\n\ttcell.KeyNames[KeyShiftX] = \"Shift-X\"\n\ttcell.KeyNames[KeyShiftY] = \"Shift-Y\"\n\ttcell.KeyNames[KeyShiftZ] = \"Shift-Z\"\n}\n"
  },
  {
    "path": "internal/ui/logo.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/tview\"\n)\n\n// Logo represents a K9s logo.\ntype Logo struct {\n\t*tview.Flex\n\n\tlogo, status *tview.TextView\n\tstyles       *config.Styles\n\tmx           sync.Mutex\n}\n\n// NewLogo returns a new logo.\nfunc NewLogo(styles *config.Styles) *Logo {\n\tl := Logo{\n\t\tFlex:   tview.NewFlex(),\n\t\tlogo:   logo(),\n\t\tstatus: status(),\n\t\tstyles: styles,\n\t}\n\tl.SetDirection(tview.FlexRow)\n\tl.AddItem(l.logo, 6, 1, false)\n\tl.AddItem(l.status, 1, 1, false)\n\tl.refreshLogo(styles.Body().LogoColor)\n\tl.SetBackgroundColor(styles.BgColor())\n\tstyles.AddListener(&l)\n\n\treturn &l\n}\n\n// Logo returns the logo viewer.\nfunc (l *Logo) Logo() *tview.TextView {\n\treturn l.logo\n}\n\n// Status returns the status viewer.\nfunc (l *Logo) Status() *tview.TextView {\n\treturn l.status\n}\n\n// StylesChanged notifies the skin changed.\nfunc (l *Logo) StylesChanged(s *config.Styles) {\n\tl.styles = s\n\tl.SetBackgroundColor(l.styles.BgColor())\n\tl.status.SetBackgroundColor(l.styles.BgColor())\n\tl.logo.SetBackgroundColor(l.styles.BgColor())\n\tl.refreshLogo(l.styles.Body().LogoColor)\n}\n\n// IsBenchmarking checks if benchmarking is active or not.\nfunc (l *Logo) IsBenchmarking() bool {\n\ttxt := l.Status().GetText(true)\n\treturn strings.Contains(txt, \"Bench\")\n}\n\n// Reset clears out the logo view and resets colors.\nfunc (l *Logo) Reset() {\n\tl.status.Clear()\n\tl.StylesChanged(l.styles)\n}\n\n// Err displays a log error state.\nfunc (l *Logo) Err(msg string) {\n\tl.update(msg, l.styles.Body().LogoColorError)\n}\n\n// Warn displays a log warning state.\nfunc (l *Logo) Warn(msg string) {\n\tl.update(msg, l.styles.Body().LogoColorWarn)\n}\n\n// Info displays a log info state.\nfunc (l *Logo) Info(msg string) {\n\tl.update(msg, l.styles.Body().LogoColorInfo)\n}\n\nfunc (l *Logo) update(msg string, c config.Color) {\n\tl.refreshStatus(msg, c)\n\tl.refreshLogo(c)\n}\n\nfunc (l *Logo) refreshStatus(msg string, c config.Color) {\n\tl.mx.Lock()\n\tdefer l.mx.Unlock()\n\n\tl.status.SetBackgroundColor(c.Color())\n\tl.status.SetText(\n\t\tfmt.Sprintf(\"[%s::b]%s\", l.styles.Body().LogoColorMsg, msg),\n\t)\n}\n\nfunc (l *Logo) refreshLogo(c config.Color) {\n\tl.mx.Lock()\n\tdefer l.mx.Unlock()\n\tl.logo.Clear()\n\tfor i, s := range LogoSmall {\n\t\t_, _ = fmt.Fprintf(l.logo, \"[%s::b]%s\", c, s)\n\t\tif i+1 < len(LogoSmall) {\n\t\t\t_, _ = fmt.Fprintf(l.logo, \"\\n\")\n\t\t}\n\t}\n}\n\nfunc logo() *tview.TextView {\n\tv := tview.NewTextView()\n\tv.SetWordWrap(false)\n\tv.SetWrap(false)\n\tv.SetTextAlign(tview.AlignLeft)\n\tv.SetDynamicColors(true)\n\n\treturn v\n}\n\nfunc status() *tview.TextView {\n\tv := tview.NewTextView()\n\tv.SetWordWrap(false)\n\tv.SetWrap(false)\n\tv.SetTextAlign(tview.AlignCenter)\n\tv.SetDynamicColors(true)\n\n\treturn v\n}\n"
  },
  {
    "path": "internal/ui/logo_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNewLogoView(t *testing.T) {\n\tv := ui.NewLogo(config.NewStyles())\n\tv.Reset()\n\n\tconst elogo = \"[#ffa500::b] ____  __ ________       \\n[#ffa500::b]|    |/  /   __   \\\\______\\n[#ffa500::b]|       /\\\\____    /  ___/\\n[#ffa500::b]|    \\\\   \\\\  /    /\\\\___  \\\\\\n[#ffa500::b]|____|\\\\__ \\\\/____//____  /\\n[#ffa500::b]         \\\\/           \\\\/ \\n\"\n\tassert.Equal(t, elogo, v.Logo().GetText(false))\n\tassert.Empty(t, v.Status().GetText(false))\n}\n\nfunc TestLogoStatus(t *testing.T) {\n\tuu := map[string]struct {\n\t\tlogo, msg, e string\n\t}{\n\t\t\"info\": {\n\t\t\t\"[#008000::b] ____  __ ________       \\n[#008000::b]|    |/  /   __   \\\\______\\n[#008000::b]|       /\\\\____    /  ___/\\n[#008000::b]|    \\\\   \\\\  /    /\\\\___  \\\\\\n[#008000::b]|____|\\\\__ \\\\/____//____  /\\n[#008000::b]         \\\\/           \\\\/ \\n\",\n\t\t\t\"blee\",\n\t\t\t\"[#ffffff::b]blee\\n\",\n\t\t},\n\t\t\"warn\": {\n\t\t\t\"[#c71585::b] ____  __ ________       \\n[#c71585::b]|    |/  /   __   \\\\______\\n[#c71585::b]|       /\\\\____    /  ___/\\n[#c71585::b]|    \\\\   \\\\  /    /\\\\___  \\\\\\n[#c71585::b]|____|\\\\__ \\\\/____//____  /\\n[#c71585::b]         \\\\/           \\\\/ \\n\",\n\t\t\t\"blee\",\n\t\t\t\"[#ffffff::b]blee\\n\",\n\t\t},\n\t\t\"err\": {\n\t\t\t\"[#ff0000::b] ____  __ ________       \\n[#ff0000::b]|    |/  /   __   \\\\______\\n[#ff0000::b]|       /\\\\____    /  ___/\\n[#ff0000::b]|    \\\\   \\\\  /    /\\\\___  \\\\\\n[#ff0000::b]|____|\\\\__ \\\\/____//____  /\\n[#ff0000::b]         \\\\/           \\\\/ \\n\",\n\t\t\t\"blee\",\n\t\t\t\"[#ffffff::b]blee\\n\",\n\t\t},\n\t}\n\n\tv := ui.NewLogo(config.NewStyles())\n\tfor n := range uu {\n\t\tk, u := n, uu[n]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tswitch k {\n\t\t\tcase \"info\":\n\t\t\t\tv.Info(u.msg)\n\t\t\tcase \"warn\":\n\t\t\t\tv.Warn(u.msg)\n\t\t\tcase \"err\":\n\t\t\t\tv.Err(u.msg)\n\t\t\t}\n\t\t\tassert.Equal(t, u.logo, v.Logo().GetText(false))\n\t\t\tassert.Equal(t, u.e, v.Status().GetText(false))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/ui/menu.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/tview\"\n\trunewidth \"github.com/mattn/go-runewidth\"\n)\n\nconst (\n\tmenuIndexFmt = \" [key:-:b]<%d> [fg:-:fgstyle]%s \"\n\tmaxRows      = 6\n)\n\nvar menuRX = regexp.MustCompile(`\\d`)\n\n// Menu presents menu options.\ntype Menu struct {\n\t*tview.Table\n\n\tstyles *config.Styles\n}\n\n// NewMenu returns a new menu.\nfunc NewMenu(styles *config.Styles) *Menu {\n\tm := Menu{\n\t\tTable:  tview.NewTable(),\n\t\tstyles: styles,\n\t}\n\tm.SetBackgroundColor(styles.BgColor())\n\tstyles.AddListener(&m)\n\n\treturn &m\n}\n\n// StylesChanged notifies skin changed.\nfunc (m *Menu) StylesChanged(s *config.Styles) {\n\tm.styles = s\n\tm.SetBackgroundColor(s.BgColor())\n\tfor row := range m.GetRowCount() {\n\t\tfor col := range m.GetColumnCount() {\n\t\t\tif c := m.GetCell(row, col); c != nil {\n\t\t\t\tc.BackgroundColor = s.BgColor()\n\t\t\t}\n\t\t}\n\t}\n}\n\n// StackPushed notifies a component was added.\nfunc (m *Menu) StackPushed(c model.Component) {\n\tm.HydrateMenu(c.Hints())\n}\n\n// StackPopped notifies a component was removed.\nfunc (m *Menu) StackPopped(_, top model.Component) {\n\tif top != nil {\n\t\tm.HydrateMenu(top.Hints())\n\t} else {\n\t\tm.Clear()\n\t}\n}\n\n// StackTop notifies the top component.\nfunc (m *Menu) StackTop(t model.Component) {\n\tm.HydrateMenu(t.Hints())\n}\n\n// HydrateMenu populate menu ui from hints.\nfunc (m *Menu) HydrateMenu(hh model.MenuHints) {\n\tm.Clear()\n\tsort.Sort(hh)\n\n\ttable := make([]model.MenuHints, maxRows+1)\n\tcolCount := (len(hh) / maxRows) + 1\n\tif m.hasDigits(hh) {\n\t\tcolCount++\n\t}\n\tfor row := range maxRows {\n\t\ttable[row] = make(model.MenuHints, colCount)\n\t}\n\tt := m.buildMenuTable(hh, table, colCount)\n\n\tfor row := range t {\n\t\tfor col := range len(t[row]) {\n\t\t\tc := tview.NewTableCell(t[row][col])\n\t\t\tif t[row][col] == \"\" {\n\t\t\t\tc = tview.NewTableCell(\"\")\n\t\t\t}\n\t\t\tc.SetBackgroundColor(m.styles.BgColor())\n\t\t\tm.SetCell(row, col, c)\n\t\t}\n\t}\n}\n\nfunc (*Menu) hasDigits(hh model.MenuHints) bool {\n\tfor _, h := range hh {\n\t\tif !h.Visible {\n\t\t\tcontinue\n\t\t}\n\t\tif menuRX.MatchString(h.Mnemonic) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (m *Menu) buildMenuTable(hh model.MenuHints, table []model.MenuHints, colCount int) [][]string {\n\tvar row, col int\n\tfirstCmd := true\n\tmaxKeys := make([]int, colCount)\n\tfor _, h := range hh {\n\t\tif !h.Visible {\n\t\t\tcontinue\n\t\t}\n\n\t\tif !menuRX.MatchString(h.Mnemonic) && firstCmd {\n\t\t\trow, col, firstCmd = 0, col+1, false\n\t\t\tif table[0][0].IsBlank() {\n\t\t\t\tcol = 0\n\t\t\t}\n\t\t}\n\t\tif maxKeys[col] < len(h.Mnemonic) {\n\t\t\tmaxKeys[col] = len(h.Mnemonic)\n\t\t}\n\t\ttable[row][col] = h\n\t\trow++\n\t\tif row >= maxRows {\n\t\t\trow, col = 0, col+1\n\t\t}\n\t}\n\n\tout := make([][]string, len(table))\n\tfor r := range out {\n\t\tout[r] = make([]string, len(table[r]))\n\t}\n\tm.layout(table, maxKeys, out)\n\n\treturn out\n}\n\nfunc (m *Menu) layout(table []model.MenuHints, mm []int, out [][]string) {\n\tfor r := range table {\n\t\tfor c := range table[r] {\n\t\t\tout[r][c] = m.formatMenu(table[r][c], mm[c])\n\t\t}\n\t}\n}\n\nfunc (m *Menu) formatMenu(h model.MenuHint, size int) string {\n\tif h.Mnemonic == \"\" || h.Description == \"\" {\n\t\treturn \"\"\n\t}\n\tstyles := m.styles.Frame()\n\ti, err := strconv.Atoi(h.Mnemonic)\n\tif err == nil {\n\t\treturn formatNSMenu(i, h.Description, &styles)\n\t}\n\n\treturn formatPlainMenu(h, size, &styles)\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc keyConv(s string) string {\n\tif s == \"\" || !strings.Contains(s, \"alt\") {\n\t\treturn s\n\t}\n\tif runtime.GOOS != \"darwin\" {\n\t\treturn s\n\t}\n\n\treturn strings.Replace(s, \"alt\", \"opt\", 1)\n}\n\n// Truncate a string to the given l and suffix ellipsis if needed.\nfunc Truncate(str string, width int) string {\n\treturn runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis))\n}\n\nfunc ToMnemonic(s string) string {\n\tif s == \"\" {\n\t\treturn s\n\t}\n\n\treturn \"<\" + keyConv(strings.ToLower(s)) + \">\"\n}\n\nfunc formatNSMenu(i int, name string, styles *config.Frame) string {\n\tfmat := strings.Replace(menuIndexFmt, \"[key\", \"[\"+styles.Menu.NumKeyColor.String(), 1)\n\tfmat = strings.ReplaceAll(fmat, \":bg:\", \":\"+styles.Title.BgColor.String()+\":\")\n\tfmat = strings.Replace(fmat, \"[fg\", \"[\"+styles.Menu.FgColor.String(), 1)\n\tfmat = strings.Replace(fmat, \"fgstyle]\", styles.Menu.FgStyle.ToShortString()+\"]\", 1)\n\n\treturn fmt.Sprintf(fmat, i, name)\n}\n\nfunc formatPlainMenu(h model.MenuHint, size int, styles *config.Frame) string {\n\tmenuFmt := \" [key:-:b]%-\" + strconv.Itoa(size+2) + \"s [fg:-:fgstyle]%s \"\n\tfmat := strings.Replace(menuFmt, \"[key\", \"[\"+styles.Menu.KeyColor.String(), 1)\n\tfmat = strings.Replace(fmat, \"[fg\", \"[\"+styles.Menu.FgColor.String(), 1)\n\tfmat = strings.ReplaceAll(fmat, \":bg:\", \":\"+styles.Title.BgColor.String()+\":\")\n\tfmat = strings.Replace(fmat, \"fgstyle]\", styles.Menu.FgStyle.ToShortString()+\"]\", 1)\n\n\treturn fmt.Sprintf(fmat, ToMnemonic(h.Mnemonic), h.Description)\n}\n"
  },
  {
    "path": "internal/ui/menu_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNewMenu(t *testing.T) {\n\tv := ui.NewMenu(config.NewStyles())\n\tv.HydrateMenu(model.MenuHints{\n\t\t{Mnemonic: \"a\", Description: \"bleeA\", Visible: true},\n\t\t{Mnemonic: \"b\", Description: \"bleeB\", Visible: true},\n\t\t{Mnemonic: \"0\", Description: \"zero\", Visible: true},\n\t})\n\n\tassert.Equal(t, \" [#ff00ff:-:b]<0> [#ffffff:-:d]zero \", v.GetCell(0, 0).Text)\n\tassert.Equal(t, \" [#1e90ff:-:b]<a> [#ffffff:-:d]bleeA \", v.GetCell(0, 1).Text)\n\tassert.Equal(t, \" [#1e90ff:-:b]<b> [#ffffff:-:d]bleeB \", v.GetCell(1, 1).Text)\n}\n\nfunc TestActionHints(t *testing.T) {\n\tuu := map[string]struct {\n\t\taa *ui.KeyActions\n\t\te  model.MenuHints\n\t}{\n\t\t\"a\": {\n\t\t\taa: ui.NewKeyActionsFromMap(ui.KeyMap{\n\t\t\t\tui.KeyB: ui.NewKeyAction(\"bleeB\", nil, true),\n\t\t\t\tui.KeyA: ui.NewKeyAction(\"bleeA\", nil, true),\n\t\t\t\tui.Key0: ui.NewKeyAction(\"zero\", nil, true),\n\t\t\t\tui.Key1: ui.NewKeyAction(\"one\", nil, false),\n\t\t\t}),\n\t\t\te: model.MenuHints{\n\t\t\t\t{Mnemonic: \"0\", Description: \"zero\", Visible: true},\n\t\t\t\t{Mnemonic: \"1\", Description: \"one\", Visible: false},\n\t\t\t\t{Mnemonic: \"a\", Description: \"bleeA\", Visible: true},\n\t\t\t\t{Mnemonic: \"b\", Description: \"bleeB\", Visible: true},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.aa.Hints())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/ui/modal_list.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n)\n\ntype ModalList struct {\n\t*tview.Box\n\n\t// The list embedded in the modal's frame.\n\tlist *tview.List\n\n\t// The frame embedded in the modal.\n\tframe *tview.Frame\n\n\t// The optional callback for when the user clicked one of the items. It\n\t// receives the index of the clicked item and the item's text.\n\tdone func(int, string)\n}\n\nfunc NewModalList(title string, list *tview.List) *ModalList {\n\tm := &ModalList{Box: tview.NewBox()}\n\n\tm.list = list\n\tm.list.SetBackgroundColor(tview.Styles.ContrastBackgroundColor).SetBorderPadding(0, 0, 0, 0)\n\tm.list.SetSelectedFunc(func(i int, main string, _ string, _ rune) {\n\t\tif m.done != nil {\n\t\t\tm.done(i, main)\n\t\t}\n\t})\n\tm.list.SetDoneFunc(func() {\n\t\tif m.done != nil {\n\t\t\tm.done(-1, \"\")\n\t\t}\n\t})\n\n\tm.frame = tview.NewFrame(m.list).SetBorders(0, 0, 1, 0, 0, 0)\n\tm.frame.SetBorder(true).\n\t\tSetBackgroundColor(tview.Styles.ContrastBackgroundColor).\n\t\tSetBorderPadding(1, 1, 1, 1)\n\tm.frame.SetTitle(title)\n\tm.frame.SetTitleColor(tcell.ColorAqua)\n\n\treturn m\n}\n\n// Draw draws this primitive onto the screen.\nfunc (m *ModalList) Draw(screen tcell.Screen) {\n\t// Calculate the width of this modal.\n\twidth := 0\n\tfor i := range m.list.GetItemCount() {\n\t\tmain, secondary := m.list.GetItemText(i)\n\t\twidth = max(width, len(main)+len(secondary)+2)\n\t}\n\n\tscreenWidth, screenHeight := screen.Size()\n\n\t// Set the modal's position and size.\n\theight := m.list.GetItemCount() + 4\n\twidth += 2\n\tx := (screenWidth - width) / 2\n\ty := (screenHeight - height) / 2\n\tm.SetRect(x, y, width, height)\n\n\t// Draw the frame.\n\tm.frame.SetRect(x, y, width, height)\n\tm.frame.Draw(screen)\n}\n\nfunc (m *ModalList) SetDoneFunc(handler func(int, string)) *ModalList {\n\tm.done = handler\n\treturn m\n}\n\n// Focus is called when this primitive receives focus.\nfunc (m *ModalList) Focus(delegate func(p tview.Primitive)) {\n\tdelegate(m.list)\n}\n\n// HasFocus returns whether this primitive has focus.\nfunc (m *ModalList) HasFocus() bool {\n\treturn m.list.HasFocus()\n}\n\n// MouseHandler returns the mouse handler for this primitive.\nfunc (m *ModalList) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) {\n\treturn m.WrapMouseHandler(func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) {\n\t\t// Pass mouse events on to the form.\n\t\tconsumed, capture = m.list.MouseHandler()(action, event, setFocus)\n\t\tif !consumed && action == tview.MouseLeftClick && m.InRect(event.Position()) {\n\t\t\tsetFocus(m)\n\t\t\tconsumed = true\n\t\t}\n\t\treturn\n\t})\n}\n\n// InputHandler returns the handler for this primitive.\nfunc (m *ModalList) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {\n\treturn m.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {\n\t\tif m.frame.HasFocus() {\n\t\t\tif handler := m.frame.InputHandler(); handler != nil {\n\t\t\t\thandler(event, setFocus)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/ui/padding.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"strings\"\n\t\"unicode\"\n\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n)\n\n// MaxyPad tracks uniform column padding.\ntype MaxyPad []int\n\n// ComputeMaxColumns figures out column max size and necessary padding.\nfunc ComputeMaxColumns(pads MaxyPad, sortColName string, t *model1.TableData) {\n\tconst colPadding = 1\n\n\tfor i, n := range t.ColumnNames(true) {\n\t\tpads[i] = len(n)\n\t\tif n == sortColName {\n\t\t\tpads[i] += 2\n\t\t}\n\t}\n\n\tvar row int\n\tt.RowsRange(func(_ int, re model1.RowEvent) bool {\n\t\tfor index, field := range re.Row.Fields {\n\t\t\twidth := len(field) + colPadding\n\t\t\tif index < len(pads) && width > pads[index] {\n\t\t\t\tpads[index] = width\n\t\t\t}\n\t\t}\n\t\trow++\n\t\treturn true\n\t})\n}\n\n// IsASCII checks if table cell has all ascii characters.\nfunc IsASCII(s string) bool {\n\tfor i := range s {\n\t\tif s[i] > unicode.MaxASCII {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\n// Pad a string up to the given length or truncates if greater than length.\nfunc Pad(s string, width int) string {\n\tif len(s) == width {\n\t\treturn s\n\t}\n\tif len(s) > width {\n\t\treturn render.Truncate(s, width)\n\t}\n\treturn s + strings.Repeat(\" \", width-len(s))\n}\n"
  },
  {
    "path": "internal/ui/padding_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestMaxColumn(t *testing.T) {\n\tuu := map[string]struct {\n\t\tt *model1.TableData\n\t\ts string\n\t\te MaxyPad\n\t}{\n\t\t\"ascii col 0\": {\n\t\t\tmodel1.NewTableDataWithRows(\n\t\t\t\tclient.NewGVR(\"test\"),\n\t\t\t\tmodel1.Header{model1.HeaderColumn{Name: \"A\"}, model1.HeaderColumn{Name: \"B\"}},\n\t\t\t\tmodel1.NewRowEventsWithEvts(\n\t\t\t\t\tmodel1.RowEvent{\n\t\t\t\t\t\tRow: model1.Row{\n\t\t\t\t\t\t\tFields: model1.Fields{\"hello\", \"world\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tmodel1.RowEvent{\n\t\t\t\t\t\tRow: model1.Row{\n\t\t\t\t\t\t\tFields: model1.Fields{\"yo\", \"mama\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t),\n\t\t\t\"A\",\n\t\t\tMaxyPad{6, 6},\n\t\t},\n\t\t\"ascii col 1\": {\n\t\t\tmodel1.NewTableDataWithRows(\n\t\t\t\tclient.NewGVR(\"test\"),\n\t\t\t\tmodel1.Header{model1.HeaderColumn{Name: \"A\"}, model1.HeaderColumn{Name: \"B\"}},\n\t\t\t\tmodel1.NewRowEventsWithEvts(\n\t\t\t\t\tmodel1.RowEvent{\n\t\t\t\t\t\tRow: model1.Row{\n\t\t\t\t\t\t\tFields: model1.Fields{\"hello\", \"world\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tmodel1.RowEvent{\n\t\t\t\t\t\tRow: model1.Row{\n\t\t\t\t\t\t\tFields: model1.Fields{\"yo\", \"mama\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t),\n\t\t\t\"B\",\n\t\t\tMaxyPad{6, 6},\n\t\t},\n\t\t\"non_ascii\": {\n\t\t\tmodel1.NewTableDataWithRows(\n\t\t\t\tclient.NewGVR(\"test\"),\n\t\t\t\tmodel1.Header{model1.HeaderColumn{Name: \"A\"}, model1.HeaderColumn{Name: \"B\"}},\n\t\t\t\tmodel1.NewRowEventsWithEvts(\n\t\t\t\t\tmodel1.RowEvent{\n\t\t\t\t\t\tRow: model1.Row{\n\t\t\t\t\t\t\tFields: model1.Fields{\"Hello World lord of ipsums 😅\", \"world\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tmodel1.RowEvent{\n\t\t\t\t\t\tRow: model1.Row{\n\t\t\t\t\t\t\tFields: model1.Fields{\"o\", \"mama\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t),\n\t\t\t),\n\t\t\t\"A\",\n\t\t\tMaxyPad{32, 6},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tpads := make(MaxyPad, u.t.HeaderCount())\n\t\t\tComputeMaxColumns(pads, u.s, u.t)\n\t\t\tassert.Equal(t, u.e, pads)\n\t\t})\n\t}\n}\n\nfunc TestIsASCII(t *testing.T) {\n\tuu := []struct {\n\t\ts string\n\t\te bool\n\t}{\n\t\t{\"hello\", true},\n\t\t{\"Yo! 😄\", false},\n\t\t{\"😄\", false},\n\t}\n\n\tfor _, u := range uu {\n\t\tassert.Equal(t, u.e, IsASCII(u.s))\n\t}\n}\n\nfunc TestPad(t *testing.T) {\n\tuu := []struct {\n\t\ts string\n\t\tl int\n\t\te string\n\t}{\n\t\t{\"fred\", 3, \"fr…\"},\n\t\t{\"01234567890\", 10, \"012345678…\"},\n\t\t{\"fred\", 10, \"fred      \"},\n\t\t{\"fred\", 6, \"fred  \"},\n\t\t{\"fred\", 4, \"fred\"},\n\t}\n\n\tfor _, u := range uu {\n\t\tassert.Equal(t, u.e, Pad(u.s, u.l))\n\t}\n}\n\nfunc BenchmarkMaxColumn(b *testing.B) {\n\ttable := model1.NewTableDataWithRows(\n\t\tclient.NewGVR(\"test\"),\n\t\tmodel1.Header{model1.HeaderColumn{Name: \"A\"}, model1.HeaderColumn{Name: \"B\"}},\n\t\tmodel1.NewRowEventsWithEvts(\n\t\t\tmodel1.RowEvent{\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"hello\", \"world\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodel1.RowEvent{\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"yo\", \"mama\"},\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t)\n\n\tpads := make(MaxyPad, table.HeaderCount())\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor range b.N {\n\t\tComputeMaxColumns(pads, \"A\", table)\n\t}\n}\n"
  },
  {
    "path": "internal/ui/pages.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/tview\"\n)\n\n// Pages represents a stack of view pages.\ntype Pages struct {\n\t*tview.Pages\n\t*model.Stack\n}\n\n// NewPages return a new view.\nfunc NewPages() *Pages {\n\tp := Pages{\n\t\tPages: tview.NewPages(),\n\t\tStack: model.NewStack(),\n\t}\n\tp.AddListener(&p)\n\n\treturn &p\n}\n\n// IsTopDialog checks if front page is a dialog.\nfunc (p *Pages) IsTopDialog() bool {\n\t_, pa := p.GetFrontPage()\n\tswitch pa.(type) {\n\tcase *tview.ModalForm, *ModalList:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// Show displays a given page.\nfunc (p *Pages) Show(c model.Component) {\n\tp.SwitchToPage(componentID(c))\n}\n\n// Current returns the current component.\nfunc (p *Pages) Current() model.Component {\n\tc := p.CurrentPage()\n\tif c == nil {\n\t\treturn nil\n\t}\n\n\treturn c.Item.(model.Component)\n}\n\n// AddAndShow adds a new page and bring it to front.\nfunc (p *Pages) addAndShow(c model.Component) {\n\tp.add(c)\n\tp.Show(c)\n}\n\n// Add adds a new page.\nfunc (p *Pages) add(c model.Component) {\n\tp.AddPage(componentID(c), c, true, true)\n}\n\n// Delete removes a page.\nfunc (p *Pages) delete(c model.Component) {\n\tp.RemovePage(componentID(c))\n}\n\n// Dump for debug.\nfunc (p *Pages) Dump() {\n\tslog.Debug(\"Dumping Pages\", slogs.Page, p)\n\tfor i, c := range p.Peek() {\n\t\tslog.Debug(fmt.Sprintf(\"%d -- %s -- %#v\", i, componentID(c), p.GetPrimitive(componentID(c))))\n\t}\n}\n\n// Stack Protocol...\n\n// StackPushed notifies a new component was pushed.\nfunc (p *Pages) StackPushed(c model.Component) {\n\tp.addAndShow(c)\n}\n\n// StackPopped notifies a component was removed.\nfunc (p *Pages) StackPopped(o, _ model.Component) {\n\tp.delete(o)\n}\n\n// StackTop notifies a new component is at the top of the stack.\nfunc (p *Pages) StackTop(top model.Component) {\n\tif top == nil {\n\t\treturn\n\t}\n\tp.Show(top)\n}\n\n// Helpers...\n\nfunc componentID(c model.Component) string {\n\tif c.Name() == \"\" {\n\t\tslog.Error(\"Component has no name\", slogs.Component, fmt.Sprintf(\"%T\", c))\n\t}\n\treturn fmt.Sprintf(\"%s-%p\", c.Name(), c)\n}\n"
  },
  {
    "path": "internal/ui/pages_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestPagesPush(t *testing.T) {\n\tc1, c2 := makeComponent(\"c1\"), makeComponent(\"c2\")\n\n\tp := ui.NewPages()\n\tp.Push(c1)\n\tp.Push(c2)\n\n\tassert.Equal(t, 2, p.GetPageCount())\n\tassert.Equal(t, c2, p.CurrentPage().Item)\n}\n\nfunc TestPagesPop(t *testing.T) {\n\tc1, c2 := makeComponent(\"c1\"), makeComponent(\"c2\")\n\n\tp := ui.NewPages()\n\tp.Push(c1)\n\tp.Push(c2)\n\tp.Pop()\n\n\tassert.Equal(t, 1, p.GetPageCount())\n\tassert.Equal(t, c1, p.CurrentPage().Item)\n}\n"
  },
  {
    "path": "internal/ui/prompt.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\t\"unicode\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n)\n\nconst (\n\tdefaultPrompt = \"%c%c [::b]%s\"\n\tdefaultSpacer = 4\n)\n\nvar (\n\t_ PromptModel = (*model.FishBuff)(nil)\n\t_ Suggester   = (*model.FishBuff)(nil)\n)\n\n// Suggester provides suggestions.\ntype Suggester interface {\n\t// CurrentSuggestion returns the current suggestion.\n\tCurrentSuggestion() (string, bool)\n\n\t// NextSuggestion returns the next suggestion.\n\tNextSuggestion() (string, bool)\n\n\t// PrevSuggestion returns the prev suggestion.\n\tPrevSuggestion() (string, bool)\n\n\t// ClearSuggestions clear out all suggestions.\n\tClearSuggestions()\n}\n\n// PromptModel represents a prompt buffer.\ntype PromptModel interface {\n\t// SetText sets the model text.\n\tSetText(txt, sug string, clear bool)\n\n\t// GetText returns the current text.\n\tGetText() string\n\n\t// GetSuggestion returns the current suggestion.\n\tGetSuggestion() string\n\n\t// ClearText clears out model text.\n\tClearText(fire bool)\n\n\t// Notify notifies all listener of current suggestions.\n\tNotify(bool)\n\n\t// AddListener registers a command listener.\n\tAddListener(model.BuffWatcher)\n\n\t// RemoveListener removes a listener.\n\tRemoveListener(model.BuffWatcher)\n\n\t// IsActive returns true if prompt is active.\n\tIsActive() bool\n\n\t// SetActive sets whether the prompt is active or not.\n\tSetActive(bool)\n\n\t// Add adds a new char to the prompt.\n\tAdd(rune)\n\n\t// Delete deletes the last prompt character.\n\tDelete()\n}\n\n// Prompt captures users free from command input.\ntype Prompt struct {\n\t*tview.TextView\n\n\tapp     *App\n\tnoIcons bool\n\ticon    rune\n\tprefix  rune\n\tstyles  *config.Styles\n\tmodel   PromptModel\n\tspacer  int\n\tmx      sync.RWMutex\n}\n\n// NewPrompt returns a new command view.\nfunc NewPrompt(app *App, noIcons bool, styles *config.Styles) *Prompt {\n\tp := Prompt{\n\t\tapp:      app,\n\t\tstyles:   styles,\n\t\tnoIcons:  noIcons,\n\t\tTextView: tview.NewTextView(),\n\t\tspacer:   defaultSpacer,\n\t}\n\tif noIcons {\n\t\tp.spacer--\n\t}\n\tp.SetWordWrap(true)\n\tp.SetWrap(true)\n\tp.SetDynamicColors(true)\n\tp.SetBorder(true)\n\tp.SetBorderPadding(0, 0, 1, 1)\n\tstyles.AddListener(&p)\n\tp.SetInputCapture(p.keyboard)\n\n\treturn &p\n}\n\n// SendKey sends a keyboard event (testing only!).\nfunc (p *Prompt) SendKey(evt *tcell.EventKey) {\n\tp.keyboard(evt)\n}\n\n// SendStrokes (testing only!)\nfunc (p *Prompt) SendStrokes(s string) {\n\tfor _, r := range s {\n\t\tp.keyboard(tcell.NewEventKey(tcell.KeyRune, r, tcell.ModNone))\n\t}\n}\n\n// Deactivate sets the prompt as inactive.\nfunc (p *Prompt) Deactivate() {\n\tif p.model != nil {\n\t\tp.model.ClearText(true)\n\t\tp.model.SetActive(false)\n\t}\n}\n\n// SetModel sets the prompt buffer model.\nfunc (p *Prompt) SetModel(m PromptModel) {\n\tif p.model != nil {\n\t\tp.model.RemoveListener(p)\n\t}\n\tp.model = m\n\tp.model.AddListener(p)\n}\n\nfunc (p *Prompt) keyboard(evt *tcell.EventKey) *tcell.EventKey {\n\tm, ok := p.model.(Suggester)\n\tif !ok {\n\t\treturn evt\n\t}\n\n\t//nolint:exhaustive\n\tswitch evt.Key() {\n\tcase tcell.KeyBackspace2, tcell.KeyBackspace, tcell.KeyDelete:\n\t\tp.model.Delete()\n\n\tcase tcell.KeyRune:\n\t\tr := evt.Rune()\n\t\t// Filter out control characters and non-printable runes that may come from\n\t\t// terminal escape sequences (e.g., cursor position reports like [7;15R)\n\t\t// Only accept printable characters for user input\n\t\tif isValidInputRune(r) {\n\t\t\tp.model.Add(r)\n\t\t}\n\n\tcase tcell.KeyEscape:\n\t\tp.model.ClearText(true)\n\t\tp.model.SetActive(false)\n\n\tcase tcell.KeyEnter, tcell.KeyCtrlE:\n\t\tp.model.SetText(p.model.GetText(), \"\", true)\n\t\tp.model.SetActive(false)\n\n\tcase tcell.KeyCtrlW, tcell.KeyCtrlU:\n\t\tp.model.ClearText(true)\n\n\tcase tcell.KeyUp:\n\t\tif s, ok := m.NextSuggestion(); ok {\n\t\t\tp.model.SetText(p.model.GetText(), s, true)\n\t\t}\n\n\tcase tcell.KeyDown:\n\t\tif s, ok := m.PrevSuggestion(); ok {\n\t\t\tp.model.SetText(p.model.GetText(), s, true)\n\t\t}\n\n\tcase tcell.KeyTab, tcell.KeyRight, tcell.KeyCtrlF:\n\t\tif s, ok := m.CurrentSuggestion(); ok {\n\t\t\tp.model.SetText(p.model.GetText()+s, \"\", true)\n\t\t\tm.ClearSuggestions()\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// StylesChanged notifies skin changed.\nfunc (p *Prompt) StylesChanged(s *config.Styles) {\n\tp.styles = s\n\tp.SetBackgroundColor(s.K9s.Prompt.BgColor.Color())\n\tp.SetTextColor(s.K9s.Prompt.FgColor.Color())\n}\n\n// InCmdMode returns true if command is active, false otherwise.\nfunc (p *Prompt) InCmdMode() bool {\n\tif p.model == nil {\n\t\treturn false\n\t}\n\treturn p.model.IsActive()\n}\n\nfunc (p *Prompt) activate() {\n\tp.Clear()\n\tp.SetCursorIndex(len(p.model.GetText()))\n\tp.write(p.model.GetText(), p.model.GetSuggestion())\n\tp.model.Notify(false)\n}\n\nfunc (p *Prompt) Clear() {\n\tp.mx.Lock()\n\tdefer p.mx.Unlock()\n\n\tp.TextView.Clear()\n}\n\nfunc (p *Prompt) Draw(sc tcell.Screen) {\n\tp.mx.RLock()\n\tdefer p.mx.RUnlock()\n\n\tp.TextView.Draw(sc)\n}\n\nfunc (p *Prompt) update(text, suggestion string) {\n\tp.Clear()\n\tp.write(text, suggestion)\n}\n\nfunc (p *Prompt) write(text, suggest string) {\n\tp.mx.Lock()\n\tdefer p.mx.Unlock()\n\n\tp.SetCursorIndex(p.spacer + len(text))\n\tif suggest != \"\" {\n\t\ttext += fmt.Sprintf(\"[%s::-]%s\", p.styles.Prompt().SuggestColor, suggest)\n\t}\n\tp.StylesChanged(p.styles)\n\t_, _ = fmt.Fprintf(p, defaultPrompt, p.icon, p.prefix, text)\n}\n\n// ----------------------------------------------------------------------------\n// Event Listener protocol...\n\n// BufferCompleted indicates input was accepted.\nfunc (p *Prompt) BufferCompleted(text, suggestion string) {\n\tp.update(text, suggestion)\n}\n\n// BufferChanged indicates the buffer was changed.\nfunc (p *Prompt) BufferChanged(text, suggestion string) {\n\tp.update(text, suggestion)\n}\n\n// SuggestionChanged notifies the suggestion changed.\nfunc (p *Prompt) SuggestionChanged(text, suggestion string) {\n\tp.update(text, suggestion)\n}\n\n// BufferActive indicates the buff activity changed.\nfunc (p *Prompt) BufferActive(activate bool, kind model.BufferKind) {\n\tif activate {\n\t\tp.ShowCursor(true)\n\t\tp.SetBorder(true)\n\t\tp.SetTextColor(p.styles.FgColor())\n\t\tp.SetBorderColor(p.colorFor(kind))\n\t\tp.icon, p.prefix = p.prefixesFor(kind)\n\t\tp.activate()\n\t\treturn\n\t}\n\n\tp.ShowCursor(false)\n\tp.SetBorder(false)\n\tp.SetBackgroundColor(p.styles.BgColor())\n\tp.Clear()\n}\n\nfunc (p *Prompt) prefixesFor(k model.BufferKind) (ic, prefix rune) {\n\tdefer func() {\n\t\tif p.noIcons {\n\t\t\tic = ' '\n\t\t}\n\t}()\n\n\t//nolint:exhaustive\n\tswitch k {\n\tcase model.CommandBuffer:\n\t\treturn '🐶', '>'\n\tdefault:\n\t\treturn '🐩', '/'\n\t}\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\n// isValidInputRune checks if a rune is valid for user input.\n// It filters out control characters and non-printable characters that may\n// come from terminal escape sequences (e.g., cursor position reports).\nfunc isValidInputRune(r rune) bool {\n\t// Reject control characters (0x00-0x1F, 0x7F) except for common whitespace\n\tif unicode.IsControl(r) && r != '\\t' && r != '\\n' && r != '\\r' {\n\t\treturn false\n\t}\n\t// Only accept printable characters\n\treturn unicode.IsPrint(r) || unicode.IsSpace(r)\n}\n\nfunc (p *Prompt) colorFor(k model.BufferKind) tcell.Color {\n\t//nolint:exhaustive\n\tswitch k {\n\tcase model.CommandBuffer:\n\t\treturn p.styles.Prompt().Border.CommandColor.Color()\n\tdefault:\n\t\treturn p.styles.Prompt().Border.DefaultColor.Color()\n\t}\n}\n"
  },
  {
    "path": "internal/ui/prompt_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCmdNew(t *testing.T) {\n\tuu := map[string]struct {\n\t\tmode   rune\n\t\tkind   model.BufferKind\n\t\tnoIcon bool\n\t\te      string\n\t}{\n\t\t\"cmd\": {\n\t\t\tmode:   ':',\n\t\t\tnoIcon: true,\n\t\t\tkind:   model.CommandBuffer,\n\t\t\te:      \" > [::b]blee\\n\",\n\t\t},\n\n\t\t\"cmd-ic\": {\n\t\t\tmode: ':',\n\t\t\tkind: model.CommandBuffer,\n\t\t\te:    \"🐶> [::b]blee\\n\",\n\t\t},\n\n\t\t\"search\": {\n\t\t\tmode:   '/',\n\t\t\tkind:   model.FilterBuffer,\n\t\t\tnoIcon: true,\n\t\t\te:      \" / [::b]blee\\n\",\n\t\t},\n\n\t\t\"search-ic\": {\n\t\t\tmode: '/',\n\t\t\tkind: model.FilterBuffer,\n\t\t\te:    \"🐩/ [::b]blee\\n\",\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tv := ui.NewPrompt(nil, u.noIcon, config.NewStyles())\n\t\t\tm := model.NewFishBuff(u.mode, u.kind)\n\t\t\tv.SetModel(m)\n\t\t\tm.AddListener(v)\n\t\t\tfor _, r := range \"blee\" {\n\t\t\t\tm.Add(r)\n\t\t\t}\n\t\t\tm.SetActive(true)\n\t\t\tassert.Equal(t, u.e, v.GetText(false))\n\t\t})\n\t}\n}\n\nfunc TestCmdUpdate(t *testing.T) {\n\tm := model.NewFishBuff(':', model.CommandBuffer)\n\tv := ui.NewPrompt(nil, true, config.NewStyles())\n\tv.SetModel(m)\n\n\tm.AddListener(v)\n\tm.SetText(\"blee\", \"\", true)\n\tm.Add('!')\n\n\tassert.Equal(t, \"\\x00\\x00 [::b]blee!\\n\", v.GetText(false))\n\tassert.False(t, v.InCmdMode())\n}\n\nfunc TestCmdMode(t *testing.T) {\n\tm := model.NewFishBuff(':', model.CommandBuffer)\n\tv := ui.NewPrompt(&ui.App{}, true, config.NewStyles())\n\tv.SetModel(m)\n\tm.AddListener(v)\n\n\tfor _, f := range []bool{false, true} {\n\t\tm.SetActive(f)\n\t\tassert.Equal(t, f, v.InCmdMode())\n\t}\n}\n\nfunc TestPrompt_Deactivate(t *testing.T) {\n\tm := model.NewFishBuff(':', model.CommandBuffer)\n\tv := ui.NewPrompt(&ui.App{}, true, config.NewStyles())\n\tv.SetModel(m)\n\tm.AddListener(v)\n\n\tm.SetActive(true)\n\tif assert.True(t, v.InCmdMode()) {\n\t\tv.Deactivate()\n\t\tassert.False(t, v.InCmdMode())\n\t}\n}\n\n// Tests that, when active, the prompt has the appropriate color\nfunc TestPromptColor(t *testing.T) {\n\tstyles := config.NewStyles()\n\tapp := ui.App{}\n\n\t// Make sure to have different values to be sure that the prompt color actually changes depending on its type\n\tassert.NotEqual(t,\n\t\tstyles.Prompt().Border.DefaultColor.Color(),\n\t\tstyles.Prompt().Border.CommandColor.Color(),\n\t)\n\n\ttestCases := []struct {\n\t\tkind          model.BufferKind\n\t\texpectedColor tcell.Color\n\t}{\n\t\t// Command prompt case\n\t\t{\n\t\t\tkind:          model.CommandBuffer,\n\t\t\texpectedColor: styles.Prompt().Border.CommandColor.Color(),\n\t\t},\n\t\t// Any other prompt type case\n\t\t{\n\t\t\t// Simulate a different type of prompt since no particular constant exists\n\t\t\tkind:          model.CommandBuffer + 1,\n\t\t\texpectedColor: styles.Prompt().Border.DefaultColor.Color(),\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tm := model.NewFishBuff(':', testCase.kind)\n\t\tprompt := ui.NewPrompt(&app, true, styles)\n\n\t\tprompt.SetModel(m)\n\t\tm.AddListener(prompt)\n\n\t\tm.SetActive(true)\n\t\tassert.Equal(t, testCase.expectedColor, prompt.GetBorderColor())\n\t}\n}\n\n// Tests that, when a change of style occurs, the prompt will have the appropriate color when active\nfunc TestPromptStyleChanged(t *testing.T) {\n\tapp := ui.App{}\n\tstyles := config.NewStyles()\n\tnewStyles := config.NewStyles()\n\tnewStyles.K9s.Prompt.Border = config.PromptBorder{\n\t\tDefaultColor: \"green\",\n\t\tCommandColor: \"yellow\",\n\t}\n\n\t// Check that the prompt won't change the border into the same style\n\tassert.NotEqual(t, styles.Prompt().Border.CommandColor.Color(), newStyles.Prompt().Border.CommandColor.Color())\n\tassert.NotEqual(t, styles.Prompt().Border.DefaultColor.Color(), newStyles.Prompt().Border.DefaultColor.Color())\n\n\ttestCases := []struct {\n\t\tkind          model.BufferKind\n\t\texpectedColor tcell.Color\n\t}{\n\t\t// Command prompt case\n\t\t{\n\t\t\tkind:          model.CommandBuffer,\n\t\t\texpectedColor: newStyles.Prompt().Border.CommandColor.Color(),\n\t\t},\n\t\t// Any other prompt type case\n\t\t{\n\t\t\t// Simulate a different type of prompt since no particular constant exists\n\t\t\tkind:          model.CommandBuffer + 1,\n\t\t\texpectedColor: newStyles.Prompt().Border.DefaultColor.Color(),\n\t\t},\n\t}\n\n\tfor _, testCase := range testCases {\n\t\tm := model.NewFishBuff(':', testCase.kind)\n\t\tprompt := ui.NewPrompt(&app, true, styles)\n\n\t\tm.SetActive(true)\n\n\t\tprompt.SetModel(m)\n\t\tm.AddListener(prompt)\n\n\t\tprompt.StylesChanged(newStyles)\n\n\t\tm.SetActive(true)\n\t\tassert.Equal(t, testCase.expectedColor, prompt.GetBorderColor())\n\t}\n}\n"
  },
  {
    "path": "internal/ui/prompt_validation_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui_test\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\n// TestPrompt_FiltersControlCharacters tests that control characters from\n// terminal escape sequences are filtered out and not added to the buffer.\nfunc TestPrompt_FiltersControlCharacters(t *testing.T) {\n\tm := model.NewFishBuff(':', model.CommandBuffer)\n\tp := ui.NewPrompt(nil, true, config.NewStyles())\n\tp.SetModel(m)\n\tm.AddListener(p)\n\tm.SetActive(true)\n\n\t// Test control characters that should be filtered\n\tcontrolChars := []rune{\n\t\t0x00, // NULL\n\t\t0x01, // SOH\n\t\t0x1B, // ESC (escape character)\n\t\t0x7F, // DEL\n\t}\n\n\tfor _, c := range controlChars {\n\t\tt.Run(fmt.Sprintf(\"control_char_0x%02X\", c), func(t *testing.T) {\n\t\t\tevt := tcell.NewEventKey(tcell.KeyRune, c, tcell.ModNone)\n\t\t\tp.SendKey(evt)\n\t\t\t// Control characters should not be added to buffer\n\t\t\tassert.Empty(t, m.GetText(), \"Control character 0x%02X should be filtered\", c)\n\t\t})\n\t}\n}\n\n// TestPrompt_AcceptsPrintableCharacters tests that valid printable\n// characters are accepted and added to the buffer.\nfunc TestPrompt_AcceptsPrintableCharacters(t *testing.T) {\n\tm := model.NewFishBuff(':', model.CommandBuffer)\n\tp := ui.NewPrompt(nil, true, config.NewStyles())\n\tp.SetModel(m)\n\tm.AddListener(p)\n\tm.SetActive(true)\n\n\t// Test valid printable characters\n\tvalidChars := []rune{\n\t\t'a', 'Z', '0', '9',\n\t\t'!', '@', '#', '$',\n\t\t' ',                // space\n\t\t'[', ']', ';', 'R', // characters from escape sequences (should be accepted if typed)\n\t}\n\n\tfor _, c := range validChars {\n\t\tt.Run(fmt.Sprintf(\"valid_char_%c\", c), func(t *testing.T) {\n\t\t\tevt := tcell.NewEventKey(tcell.KeyRune, c, tcell.ModNone)\n\t\t\tp.SendKey(evt)\n\t\t\t// Valid characters should be added\n\t\t\tassert.Contains(t, m.GetText(), string(c), \"Valid character %c should be accepted\", c)\n\t\t\t// Clear for next test\n\t\t\tm.ClearText(true)\n\t\t})\n\t}\n\n\t// Test tab separately (it's a control char but should be accepted)\n\tt.Run(\"valid_char_tab\", func(t *testing.T) {\n\t\tevt := tcell.NewEventKey(tcell.KeyRune, '\\t', tcell.ModNone)\n\t\tp.SendKey(evt)\n\t\t// Tab should be accepted (it's a special case in the validation)\n\t\t// Note: Tab might be converted to spaces or handled differently by the buffer\n\t\ttext := m.GetText()\n\t\t// Tab is accepted by validation, but may be handled specially by the buffer\n\t\t// Just verify the buffer isn't empty (meaning something was processed)\n\t\tassert.NotNil(t, text, \"Tab character should be processed\")\n\t\tm.ClearText(true)\n\t})\n}\n\n// TestPrompt_FiltersEscapeSequencePattern tests that escape sequence\n// patterns are not automatically added when they appear as individual runes.\n// Note: This test verifies the validation works, but escape sequences\n// should ideally be handled by tcell before reaching KeyRune.\nfunc TestPrompt_FiltersEscapeSequencePattern(t *testing.T) {\n\tm := model.NewFishBuff(':', model.CommandBuffer)\n\tp := ui.NewPrompt(nil, true, config.NewStyles())\n\tp.SetModel(m)\n\tm.AddListener(p)\n\tm.SetActive(true)\n\n\t// Simulate the problematic escape sequence pattern [7;15R\n\t// Each character individually is printable, but we want to ensure\n\t// they don't appear unexpectedly\n\tescapeSequence := \"[7;15R\"\n\n\t// Send each character\n\tfor _, r := range escapeSequence {\n\t\tevt := tcell.NewEventKey(tcell.KeyRune, r, tcell.ModNone)\n\t\tp.SendKey(evt)\n\t}\n\n\t// The characters themselves are printable, so they will be added\n\t// This test documents the current behavior - the fix prevents\n\t// control characters, but printable escape sequence chars would\n\t// still be added if tcell doesn't filter them first\n\ttext := m.GetText()\n\n\t// If all characters are printable, they will be in the buffer\n\t// This is expected behavior - the fix prevents control chars,\n\t// but can't prevent legitimate printable characters\n\tassert.NotEmpty(t, text, \"Printable escape sequence chars may still appear\")\n\n\t// However, we can verify no control characters made it through\n\tfor _, r := range text {\n\t\tassert.False(t, isControlChar(r), \"No control characters should be in buffer\")\n\t}\n}\n\n// Helper function to check if a rune is a control character\nfunc isControlChar(r rune) bool {\n\treturn r >= 0x00 && r <= 0x1F || r == 0x7F\n}\n"
  },
  {
    "path": "internal/ui/select_table.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n)\n\n// SelectTable represents a table with selections.\ntype SelectTable struct {\n\t*tview.Table\n\n\tmodel      Tabular\n\tselectedFn func(string) string\n\tmarks      map[string]struct{}\n\tselFgColor tcell.Color\n\tselBgColor tcell.Color\n}\n\n// SetModel sets the table model.\nfunc (s *SelectTable) SetModel(m Tabular) {\n\ts.model = m\n}\n\n// GetModel returns the current model.\nfunc (s *SelectTable) GetModel() Tabular {\n\treturn s.model\n}\n\n// ClearSelection reset selected row.\nfunc (s *SelectTable) ClearSelection() {\n\ts.Select(0, 0)\n\ts.ScrollToBeginning()\n}\n\n// SelectFirstRow select first data row if any.\nfunc (s *SelectTable) SelectFirstRow() {\n\tif s.GetRowCount() > 0 {\n\t\ts.Select(1, 0)\n\t}\n}\n\n// GetSelectedItems return currently marked or selected items names.\nfunc (s *SelectTable) GetSelectedItems() []string {\n\tif len(s.marks) == 0 {\n\t\tif item := s.GetSelectedItem(); item != \"\" {\n\t\t\treturn []string{item}\n\t\t}\n\t\treturn nil\n\t}\n\n\titems := make([]string, 0, len(s.marks))\n\tfor item := range s.marks {\n\t\titems = append(items, item)\n\t}\n\n\treturn items\n}\n\n// GetRowID returns the row id at given location.\nfunc (s *SelectTable) GetRowID(index int) (string, bool) {\n\tcell := s.GetCell(index, 0)\n\tif cell == nil {\n\t\treturn \"\", false\n\t}\n\tid, ok := cell.GetReference().(string)\n\n\treturn id, ok\n}\n\n// GetSelectedItem returns the currently selected item name.\nfunc (s *SelectTable) GetSelectedItem() string {\n\tif s.GetSelectedRowIndex() == 0 || s.model.Empty() {\n\t\treturn \"\"\n\t}\n\tsel, ok := s.GetCell(s.GetSelectedRowIndex(), 0).GetReference().(string)\n\tif !ok {\n\t\treturn \"\"\n\t}\n\tif s.selectedFn != nil {\n\t\treturn s.selectedFn(sel)\n\t}\n\treturn sel\n}\n\n// GetSelectedCell returns the content of a cell for the currently selected row.\nfunc (s *SelectTable) GetSelectedCell(col int) string {\n\tr, _ := s.GetSelection()\n\treturn TrimCell(s, r, col)\n}\n\n// SetSelectedFn defines a function that cleanse the current selection.\nfunc (s *SelectTable) SetSelectedFn(f func(string) string) {\n\ts.selectedFn = f\n}\n\n// GetSelectedRowIndex fetch the currently selected row index.\nfunc (s *SelectTable) GetSelectedRowIndex() int {\n\tr, _ := s.GetSelection()\n\treturn r\n}\n\n// SelectRow select a given row by index.\nfunc (s *SelectTable) SelectRow(r, c int, broadcast bool) {\n\tif !broadcast {\n\t\ts.SetSelectionChangedFunc(nil)\n\t}\n\tif count := s.model.RowCount(); count > 0 && r-1 > count {\n\t\tr = count + 1\n\t}\n\tdefer s.SetSelectionChangedFunc(s.selectionChanged)\n\ts.Select(r, c)\n}\n\n// UpdateSelection refresh selected row.\nfunc (s *SelectTable) updateSelection(broadcast bool) {\n\tr, c := s.GetSelection()\n\ts.SelectRow(r, c, broadcast)\n}\n\nfunc (s *SelectTable) selectionChanged(r, c int) {\n\tif r < 0 {\n\t\treturn\n\t}\n\tif cell := s.GetCell(r, c); cell != nil {\n\t\ts.SetSelectedStyle(\n\t\t\ttcell.StyleDefault.Foreground(s.selFgColor).\n\t\t\t\tBackground(cell.Color).Attributes(tcell.AttrBold))\n\t}\n}\n\n// ClearMarks delete all marked items.\nfunc (s *SelectTable) ClearMarks() {\n\tfor k := range s.marks {\n\t\tdelete(s.marks, k)\n\t}\n}\n\n// DeleteMark delete a marked item.\nfunc (s *SelectTable) DeleteMark(k string) {\n\tdelete(s.marks, k)\n}\n\n// ToggleMark toggles marked row.\nfunc (s *SelectTable) ToggleMark() {\n\tsel := s.GetSelectedItem()\n\tif sel == \"\" {\n\t\treturn\n\t}\n\tif _, ok := s.marks[sel]; ok {\n\t\tdelete(s.marks, s.GetSelectedItem())\n\t} else {\n\t\ts.marks[sel] = struct{}{}\n\t}\n\n\tif cell := s.GetCell(s.GetSelectedRowIndex(), 0); cell != nil {\n\t\ts.SetSelectedStyle(tcell.StyleDefault.Foreground(cell.BackgroundColor).Background(cell.Color).Attributes(tcell.AttrBold))\n\t}\n}\n\n// SpanMark toggles marked row.\nfunc (s *SelectTable) SpanMark() {\n\tselIndex, prev := s.GetSelectedRowIndex(), -1\n\tif selIndex <= 0 {\n\t\treturn\n\t}\n\t// Look back to find previous mark\n\tfor i := selIndex - 1; i > 0; i-- {\n\t\tid, ok := s.GetRowID(i)\n\t\tif !ok {\n\t\t\tbreak\n\t\t}\n\t\tif _, ok := s.marks[id]; ok {\n\t\t\tprev = i\n\t\t\tbreak\n\t\t}\n\t}\n\tif prev != -1 {\n\t\ts.markRange(prev, selIndex)\n\t\treturn\n\t}\n\n\t// Look forward to see if we have a mark\n\tfor i := selIndex; i < s.GetRowCount(); i++ {\n\t\tid, ok := s.GetRowID(i)\n\t\tif !ok {\n\t\t\tbreak\n\t\t}\n\t\tif _, ok := s.marks[id]; ok {\n\t\t\tprev = i\n\t\t\tbreak\n\t\t}\n\t}\n\ts.markRange(prev, selIndex)\n}\n\nfunc (s *SelectTable) markRange(prev, curr int) {\n\tif prev < 0 {\n\t\treturn\n\t}\n\tif prev > curr {\n\t\tprev, curr = curr, prev\n\t}\n\tfor i := prev + 1; i <= curr; i++ {\n\t\tid, ok := s.GetRowID(i)\n\t\tif !ok {\n\t\t\tbreak\n\t\t}\n\t\ts.marks[id] = struct{}{}\n\t\tcell := s.GetCell(s.GetSelectedRowIndex(), 0)\n\t\tif cell == nil {\n\t\t\tbreak\n\t\t}\n\t\ts.SetSelectedStyle(tcell.StyleDefault.Foreground(cell.BackgroundColor).Background(cell.Color).Attributes(tcell.AttrBold))\n\t}\n}\n\n// IsMarked returns true if this item was marked.\nfunc (s *SelectTable) IsMarked(item string) bool {\n\t_, ok := s.marks[item]\n\treturn ok\n}\n"
  },
  {
    "path": "internal/ui/splash.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/tview\"\n)\n\n// LogoSmall K9s small log.\nvar LogoSmall = []string{\n\t` ____  __ ________       `,\n\t`|    |/  /   __   \\______`,\n\t`|       /\\____    /  ___/`,\n\t`|    \\   \\  /    /\\___  \\`,\n\t`|____|\\__ \\/____//____  /`,\n\t`         \\/           \\/ `,\n}\n\n// LogoBig K9s big logo for splash page.\nvar LogoBig = []string{\n\t` ____  __ ________        _______  ____     ___ `,\n\t`|    |/  /   __   \\______/   ___ \\|    |   |   |`,\n\t`|       /\\____    /  ___/    \\  \\/|    |   |   |`,\n\t`|    \\   \\  /    /\\___  \\     \\___|    |___|   |`,\n\t`|____|\\__ \\/____//____  /\\______  /_______ \\___|`,\n\t`         \\/           \\/        \\/        \\/    `,\n}\n\n// Splash represents a splash screen.\ntype Splash struct {\n\t*tview.Flex\n}\n\n// NewSplash instantiates a new splash screen with product and company info.\nfunc NewSplash(styles *config.Styles, version string) *Splash {\n\ts := Splash{Flex: tview.NewFlex()}\n\ts.SetBackgroundColor(styles.BgColor())\n\n\tlogo := tview.NewTextView()\n\tlogo.SetDynamicColors(true)\n\tlogo.SetTextAlign(tview.AlignCenter)\n\ts.layoutLogo(logo, styles)\n\n\tvers := tview.NewTextView()\n\tvers.SetDynamicColors(true)\n\tvers.SetTextAlign(tview.AlignCenter)\n\ts.layoutRev(vers, version, styles)\n\n\ts.SetDirection(tview.FlexRow)\n\ts.AddItem(logo, 10, 1, false)\n\ts.AddItem(vers, 1, 1, false)\n\n\treturn &s\n}\n\nfunc (*Splash) layoutLogo(t *tview.TextView, styles *config.Styles) {\n\tlogo := strings.Join(LogoBig, fmt.Sprintf(\"\\n[%s::b]\", styles.Body().LogoColor))\n\t_, _ = fmt.Fprintf(t, \"%s[%s::b]%s\\n\",\n\t\tstrings.Repeat(\"\\n\", 2),\n\t\tstyles.Body().LogoColor,\n\t\tlogo)\n}\n\nfunc (*Splash) layoutRev(t *tview.TextView, rev string, styles *config.Styles) {\n\t_, _ = fmt.Fprintf(t, \"[%s::b]Revision [red::b]%s\", styles.Body().FgColor, rev)\n}\n"
  },
  {
    "path": "internal/ui/splash_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestNewSplash(t *testing.T) {\n\ts := ui.NewSplash(config.NewStyles(), \"bozo\")\n\n\tx, y, w, h := s.GetRect()\n\tassert.Equal(t, 0, x)\n\tassert.Equal(t, 0, y)\n\tassert.Equal(t, 15, w)\n\tassert.Equal(t, 10, h)\n}\n"
  },
  {
    "path": "internal/ui/table.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sync\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/vul\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n)\n\nconst maxTruncate = 50\n\ntype (\n\t// ColorerFunc represents a row colorer.\n\tColorerFunc func(ns string, evt model1.RowEvent) tcell.Color\n\n\t// DecorateFunc represents a row decorator.\n\tDecorateFunc func(*model1.TableData)\n\n\t// SelectedRowFunc a table selection callback.\n\tSelectedRowFunc func(r int)\n)\n\n// Table represents tabular data.\ntype Table struct {\n\t*SelectTable\n\tgvr            *client.GVR\n\tsortCol        model1.SortColumn\n\tselectedColIdx int\n\tmanualSort     bool\n\tPath           string\n\tExtras         string\n\tactions        *KeyActions\n\tcmdBuff        *model.FishBuff\n\tstyles         *config.Styles\n\tviewSetting    *config.ViewSetting\n\tcolorerFn      model1.ColorerFunc\n\tdecorateFn     DecorateFunc\n\twide           bool\n\ttoast          bool\n\thasMetrics     bool\n\tctx            context.Context\n\tmx             sync.RWMutex\n\treadOnly       bool\n\tnoIcon         bool\n\tfullGVR        bool\n}\n\n// NewTable returns a new table view.\nfunc NewTable(gvr *client.GVR) *Table {\n\treturn &Table{\n\t\tSelectTable: &SelectTable{\n\t\t\tTable: tview.NewTable(),\n\t\t\tmodel: model.NewTable(gvr),\n\t\t\tmarks: make(map[string]struct{}),\n\t\t},\n\t\tctx:     context.Background(),\n\t\tgvr:     gvr,\n\t\tactions: NewKeyActions(),\n\t\tcmdBuff: model.NewFishBuff('/', model.FilterBuffer),\n\t\tsortCol: model1.SortColumn{ASC: true},\n\t}\n}\n\n// SetFullGVR toggles full GVR title display.\nfunc (t *Table) SetFullGVR(b bool) {\n\tt.mx.Lock()\n\tdefer t.mx.Unlock()\n\n\tt.fullGVR = b\n}\n\n// SetNoIcon toggles no icon mode.\nfunc (t *Table) SetNoIcon(b bool) {\n\tt.mx.Lock()\n\tdefer t.mx.Unlock()\n\n\tt.noIcon = b\n}\n\n// SetReadOnly toggles read-only mode.\nfunc (t *Table) SetReadOnly(ro bool) {\n\tt.mx.Lock()\n\tdefer t.mx.Unlock()\n\n\tt.readOnly = ro\n}\n\nfunc (t *Table) setSortCol(sc model1.SortColumn) {\n\tt.mx.Lock()\n\tdefer t.mx.Unlock()\n\n\tt.sortCol = sc\n}\n\nfunc (t *Table) toggleSortCol() {\n\tt.mx.Lock()\n\tdefer t.mx.Unlock()\n\n\tt.sortCol.ASC = !t.sortCol.ASC\n}\n\nfunc (t *Table) getSortCol() model1.SortColumn {\n\tt.mx.RLock()\n\tdefer t.mx.RUnlock()\n\n\treturn t.sortCol\n}\n\nfunc (t *Table) setMSort(b bool) {\n\tt.mx.Lock()\n\tdefer t.mx.Unlock()\n\n\tt.manualSort = b\n}\n\nfunc (t *Table) getMSort() bool {\n\tt.mx.RLock()\n\tdefer t.mx.RUnlock()\n\n\treturn t.manualSort\n}\n\nfunc (t *Table) getSelectedColIdx() int {\n\tt.mx.RLock()\n\tdefer t.mx.RUnlock()\n\n\treturn t.selectedColIdx\n}\n\n// initSelectedColumn initializes the selected column index based on current sort column.\nfunc (t *Table) initSelectedColumn() {\n\tdata := t.GetFilteredData()\n\tif data == nil || data.HeaderCount() == 0 {\n\t\treturn\n\t}\n\n\tsc := t.getSortCol()\n\tif sc.Name == \"\" {\n\t\tt.mx.Lock()\n\t\tt.selectedColIdx = 0\n\t\tt.mx.Unlock()\n\t\treturn\n\t}\n\n\t// Find the visual column index for the current sort column\n\theader := data.Header()\n\tvisibleCol := 0\n\tfor _, h := range header {\n\t\tif t.shouldExcludeColumn(h) {\n\t\t\tcontinue\n\t\t}\n\t\tif h.Name == sc.Name {\n\t\t\tt.mx.Lock()\n\t\t\tt.selectedColIdx = visibleCol\n\t\t\tt.mx.Unlock()\n\t\t\treturn\n\t\t}\n\t\tvisibleCol++\n\t}\n\n\t// If sort column not found in visible columns, default to 0\n\tt.mx.Lock()\n\tt.selectedColIdx = 0\n\tt.mx.Unlock()\n}\n\n// moveSelectedColumn moves the column selection by delta (-1 for left, +1 for right).\nfunc (t *Table) moveSelectedColumn(delta int) {\n\tdata := t.GetFilteredData()\n\tif data == nil || data.HeaderCount() == 0 {\n\t\treturn\n\t}\n\n\t// Count visible columns\n\tvisibleCount := 0\n\tfor _, h := range data.Header() {\n\t\tif !t.shouldExcludeColumn(h) {\n\t\t\tvisibleCount++\n\t\t}\n\t}\n\n\tif visibleCount == 0 {\n\t\treturn\n\t}\n\n\tt.mx.Lock()\n\tt.selectedColIdx += delta\n\t// Wrap around\n\tif t.selectedColIdx >= visibleCount {\n\t\tt.selectedColIdx = 0\n\t} else if t.selectedColIdx < 0 {\n\t\tt.selectedColIdx = visibleCount - 1\n\t}\n\tt.mx.Unlock()\n\n\tt.Refresh()\n}\n\n// SelectNextColumn moves the column selection to the right.\nfunc (t *Table) SelectNextColumn() {\n\tt.moveSelectedColumn(1)\n}\n\n// SelectPrevColumn moves the column selection to the left.\nfunc (t *Table) SelectPrevColumn() {\n\tt.moveSelectedColumn(-1)\n}\n\n// SortSelectedColumn sorts by the currently selected column.\nfunc (t *Table) SortSelectedColumn() {\n\tdata := t.GetFilteredData()\n\tif data == nil || data.HeaderCount() == 0 {\n\t\treturn\n\t}\n\n\tidx := t.getSelectedColIdx()\n\tif idx < 0 {\n\t\treturn\n\t}\n\n\t// Map visual column index to actual header column name\n\t// (accounting for hidden columns)\n\theader := data.Header()\n\tvisibleCol := 0\n\tvar colName string\n\tfor _, h := range header {\n\t\tif t.shouldExcludeColumn(h) {\n\t\t\tcontinue\n\t\t}\n\t\tif visibleCol == idx {\n\t\t\tcolName = h.Name\n\t\t\tbreak\n\t\t}\n\t\tvisibleCol++\n\t}\n\n\tif colName == \"\" {\n\t\treturn\n\t}\n\n\tsc := t.getSortCol()\n\n\t// Toggle direction if same column, otherwise default to ascending\n\tasc := true\n\tif sc.Name == colName {\n\t\tasc = !sc.ASC\n\t}\n\n\tt.SetSortCol(colName, asc)\n\tt.setMSort(true)\n\tt.Refresh()\n}\n\n// SetViewSetting sets custom view config is present.\nfunc (t *Table) SetViewSetting(vs *config.ViewSetting) bool {\n\tt.mx.Lock()\n\tdefer t.mx.Unlock()\n\n\tif !t.viewSetting.Equals(vs) {\n\t\tt.viewSetting = vs\n\t\tslog.Debug(\"Updating custom view setting\", slogs.GVR, t.gvr, slogs.ViewSetting, vs)\n\t\tt.model.SetViewSetting(t.ctx, vs)\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// GetViewSetting return current view settings if any.\nfunc (t *Table) GetViewSetting() *config.ViewSetting {\n\tt.mx.RLock()\n\tdefer t.mx.RUnlock()\n\n\treturn t.viewSetting\n}\n\nfunc (t *Table) GetContext() context.Context {\n\treturn t.ctx\n}\n\nfunc (t *Table) SetContext(ctx context.Context) {\n\tt.ctx = ctx\n}\n\n// Init initializes the component.\nfunc (t *Table) Init(ctx context.Context) {\n\tt.SetFixed(1, 0)\n\tt.SetBorder(true)\n\tt.SetBorderAttributes(tcell.AttrBold)\n\tt.SetBorderPadding(0, 0, 1, 1)\n\tt.SetSelectable(true, false)\n\tt.SetSelectionChangedFunc(t.selectionChanged)\n\tt.SetBackgroundColor(tcell.ColorDefault)\n\tt.Select(1, 0)\n\tt.styles = mustExtractStyles(ctx)\n\tt.StylesChanged(t.styles)\n}\n\n// GVR returns a resource descriptor.\nfunc (t *Table) GVR() *client.GVR { return t.gvr }\n\n// ViewSettingsChanged notifies listener the view configuration changed.\nfunc (t *Table) ViewSettingsChanged(vs *config.ViewSetting) {\n\tif t.SetViewSetting(vs) {\n\t\tif vs == nil {\n\t\t\tif !t.getMSort() && !t.sortCol.IsSet() {\n\t\t\t\tt.setSortCol(model1.SortColumn{})\n\t\t\t}\n\t\t} else {\n\t\t\tt.setMSort(false)\n\t\t}\n\t\tt.Refresh()\n\t}\n}\n\n// StylesChanged notifies the skin changed.\nfunc (t *Table) StylesChanged(s *config.Styles) {\n\tt.SetBackgroundColor(s.Table().BgColor.Color())\n\tt.SetBorderColor(s.Frame().Border.FgColor.Color())\n\tt.SetBorderFocusColor(s.Frame().Border.FocusColor.Color())\n\tt.SetSelectedStyle(\n\t\ttcell.StyleDefault.Foreground(t.styles.Table().CursorFgColor.Color()).\n\t\t\tBackground(t.styles.Table().CursorBgColor.Color()).Attributes(tcell.AttrBold))\n\tt.selFgColor = s.Table().CursorFgColor.Color()\n\tt.selBgColor = s.Table().CursorBgColor.Color()\n\tt.Refresh()\n}\n\n// ResetToast resets toast flag.\nfunc (t *Table) ResetToast() {\n\tt.toast = false\n\tt.Refresh()\n}\n\n// ToggleToast toggles to show toast resources.\nfunc (t *Table) ToggleToast() {\n\tt.toast = !t.toast\n\tt.Refresh()\n}\n\n// ToggleWide toggles wide col display.\nfunc (t *Table) ToggleWide() {\n\tt.wide = !t.wide\n\tt.Refresh()\n}\n\n// Actions returns active menu bindings.\nfunc (t *Table) Actions() *KeyActions {\n\treturn t.actions\n}\n\n// Styles returns styling configurator.\nfunc (t *Table) Styles() *config.Styles {\n\treturn t.styles\n}\n\n// FilterInput filters user commands.\nfunc (t *Table) FilterInput(r rune) bool {\n\tif !t.cmdBuff.IsActive() {\n\t\treturn false\n\t}\n\tt.cmdBuff.Add(r)\n\tt.ClearSelection()\n\tt.doUpdate(t.filtered(t.GetModel().Peek()))\n\tt.UpdateTitle()\n\tt.SelectFirstRow()\n\n\treturn true\n}\n\n// Filter filters out table data.\nfunc (t *Table) Filter(string) {\n\tt.ClearSelection()\n\tt.doUpdate(t.filtered(t.GetModel().Peek()))\n\tt.UpdateTitle()\n\tt.SelectFirstRow()\n}\n\n// Hints returns the view hints.\nfunc (t *Table) Hints() model.MenuHints {\n\treturn t.actions.Hints()\n}\n\n// ExtraHints returns additional hints.\nfunc (*Table) ExtraHints() map[string]string {\n\treturn nil\n}\n\n// GetFilteredData fetch filtered tabular data.\nfunc (t *Table) GetFilteredData() *model1.TableData {\n\treturn t.filtered(t.GetModel().Peek())\n}\n\n// SetDecorateFn specifies the default row decorator.\nfunc (t *Table) SetDecorateFn(f DecorateFunc) {\n\tt.decorateFn = f\n}\n\n// SetColorerFn specifies the default colorer.\nfunc (t *Table) SetColorerFn(f model1.ColorerFunc) {\n\tt.colorerFn = f\n}\n\n// SetSortCol sets in sort column index and order.\nfunc (t *Table) SetSortCol(name string, asc bool) {\n\tt.setSortCol(model1.SortColumn{Name: name, ASC: asc})\n}\n\n// Update table content.\nfunc (t *Table) Update(data *model1.TableData, hasMetrics bool) *model1.TableData {\n\tif t.decorateFn != nil {\n\t\tt.decorateFn(data)\n\t}\n\tt.hasMetrics = hasMetrics\n\n\treturn t.doUpdate(t.filtered(data))\n}\n\nfunc (t *Table) GetNamespace() string {\n\tif t.GetModel() != nil {\n\t\treturn t.GetModel().GetNamespace()\n\t}\n\n\treturn client.NamespaceAll\n}\n\nfunc (t *Table) doUpdate(data *model1.TableData) *model1.TableData {\n\tif client.IsAllNamespaces(data.GetNamespace()) {\n\t\tt.actions.Add(\n\t\t\tKeyShiftP,\n\t\t\tNewKeyAction(\"Sort Namespace\", t.SortColCmd(\"NAMESPACE\", true), false),\n\t\t)\n\t} else {\n\t\tt.actions.Delete(KeyShiftP)\n\t}\n\n\toldSortCol := t.getSortCol()\n\tt.setSortCol(data.ComputeSortCol(t.GetViewSetting(), t.getSortCol(), t.getMSort()))\n\n\t// Initialize selected column index to match the current sort column\n\t// This ensures the highlight starts at the sorted column\n\tnewSortCol := t.getSortCol()\n\tif oldSortCol.Name != newSortCol.Name {\n\t\tt.initSelectedColumn()\n\t}\n\n\treturn data\n}\n\nfunc (t *Table) shouldExcludeColumn(h model1.HeaderColumn) bool {\n\treturn (h.Hide || (!t.wide && h.Wide)) ||\n\t\t(h.Name == \"NAMESPACE\" && !t.GetModel().ClusterWide()) ||\n\t\t(h.MX && !t.hasMetrics) ||\n\t\t(h.VS && vul.ImgScanner == nil)\n}\n\nfunc (t *Table) UpdateUI(cdata, data *model1.TableData) {\n\tt.Clear()\n\tfg := t.styles.Table().Header.FgColor.Color()\n\tbg := t.styles.Table().Header.BgColor.Color()\n\n\tvar col int\n\tfor _, h := range cdata.Header() {\n\t\tif t.shouldExcludeColumn(h) {\n\t\t\tcontinue\n\t\t}\n\t\tt.AddHeaderCell(col, h)\n\t\tc := t.GetCell(0, col)\n\t\tc.SetBackgroundColor(bg)\n\t\tc.SetTextColor(fg)\n\t\tcol++\n\t}\n\tcdata.Sort(t.getSortCol())\n\n\tpads := make(MaxyPad, cdata.HeaderCount())\n\tComputeMaxColumns(pads, t.getSortCol().Name, cdata)\n\tcdata.RowsRange(func(row int, re model1.RowEvent) bool {\n\t\tore, ok := data.FindRow(re.Row.ID)\n\t\tif !ok {\n\t\t\tslog.Error(\"Unable to find original row event\", slogs.RowID, re.Row.ID)\n\t\t\treturn true\n\t\t}\n\t\tt.buildRow(row+1, re, ore, cdata.Header(), pads)\n\n\t\treturn true\n\t})\n\n\tt.updateSelection(true)\n\tt.UpdateTitle()\n}\n\nfunc (t *Table) buildRow(r int, re, ore model1.RowEvent, h model1.Header, pads MaxyPad) {\n\tcolor := model1.DefaultColorer\n\tif t.colorerFn != nil {\n\t\tcolor = t.colorerFn\n\t}\n\n\tmarked := t.IsMarked(re.Row.ID)\n\tvar col int\n\tns := t.GetModel().GetNamespace()\n\tfor c, field := range re.Row.Fields {\n\t\tif c >= len(h) {\n\t\t\tslog.Error(\"Field/header overflow detected. Check your mappings!\",\n\t\t\t\tslogs.GVR, t.GVR(),\n\t\t\t\tslogs.Cell, c,\n\t\t\t\tslogs.HeaderSize, len(h),\n\t\t\t)\n\t\t\tcontinue\n\t\t}\n\t\tif t.shouldExcludeColumn(h[c]) {\n\t\t\tcontinue\n\t\t}\n\n\t\tif !re.Deltas.IsBlank() && !h.IsTimeCol(c) {\n\t\t\tvar old string\n\t\t\tif c < len(ore.Deltas) {\n\t\t\t\told = ore.Deltas[c]\n\t\t\t}\n\t\t\tif c < len(re.Deltas) {\n\t\t\t\told = re.Deltas[c]\n\t\t\t}\n\t\t\tfield += Deltas(old, field)\n\t\t}\n\n\t\tif h[c].Decorator != nil {\n\t\t\tfield = h[c].Decorator(field)\n\t\t}\n\t\tif h[c].Align == tview.AlignLeft {\n\t\t\tfield = formatCell(field, pads[c])\n\t\t}\n\n\t\tcell := tview.NewTableCell(field)\n\t\tcell.SetExpansion(1)\n\t\tcell.SetAlign(h[c].Align)\n\t\tfgColor := color(ns, h, &re)\n\t\tcell.SetTextColor(fgColor)\n\t\tif marked {\n\t\t\tcell.SetTextColor(t.styles.Table().MarkColor.Color())\n\t\t}\n\t\tif col == 0 {\n\t\t\tcell.SetReference(re.Row.ID)\n\t\t}\n\t\tt.SetCell(r, col, cell)\n\t\tcol++\n\t}\n}\n\n// SortColCmd designates a sorted column.\nfunc (t *Table) SortColCmd(name string, asc bool) func(evt *tcell.EventKey) *tcell.EventKey {\n\treturn func(*tcell.EventKey) *tcell.EventKey {\n\t\tsc := t.getSortCol()\n\t\tsc.ASC = !sc.ASC\n\t\tif sc.Name != name {\n\t\t\tsc.ASC = asc\n\t\t}\n\t\tsc.Name = name\n\t\tt.setSortCol(sc)\n\t\tt.setMSort(true)\n\n\t\t// Sync selected column index with the new sort column\n\t\tt.initSelectedColumn()\n\n\t\tt.Refresh()\n\t\treturn nil\n\t}\n}\n\n// SortInvertCmd reverses sorting order.\nfunc (t *Table) SortInvertCmd(*tcell.EventKey) *tcell.EventKey {\n\tt.toggleSortCol()\n\tt.Refresh()\n\n\treturn nil\n}\n\n// ClearMarks clear out marked items.\nfunc (t *Table) ClearMarks() {\n\tt.SelectTable.ClearMarks()\n\tt.Refresh()\n}\n\n// Refresh update the table data.\nfunc (t *Table) Refresh() {\n\tdata := t.model.Peek()\n\tif data.HeaderCount() == 0 {\n\t\treturn\n\t}\n\tcdata := t.Update(data, t.hasMetrics)\n\tt.UpdateUI(cdata, data)\n}\n\n// GetSelectedRow returns the entire selected row or nil if nothing selected.\nfunc (t *Table) GetSelectedRow(path string) *model1.Row {\n\tdata := t.model.Peek()\n\tre, ok := data.FindRow(path)\n\tif !ok {\n\t\treturn nil\n\t}\n\n\treturn &re.Row\n}\n\n// NameColIndex returns the index of the resource name column.\nfunc (t *Table) NameColIndex() int {\n\tcol := 0\n\tif client.IsClusterScoped(t.GetModel().GetNamespace()) {\n\t\treturn col\n\t}\n\tif t.GetModel().ClusterWide() {\n\t\tcol++\n\t}\n\n\treturn col\n}\n\n// AddHeaderCell configures a table cell header.\nfunc (t *Table) AddHeaderCell(col int, h model1.HeaderColumn) {\n\tsc := t.getSortCol()\n\tsortCol := h.Name == sc.Name\n\tselectedCol := col == t.getSelectedColIdx()\n\tstyles := t.styles.Table()\n\tc := tview.NewTableCell(columnIndicator(sortCol, selectedCol, sc.ASC, &styles, h.Name))\n\tc.SetExpansion(1)\n\tc.SetSelectable(false)\n\tc.SetAlign(h.Align)\n\tt.SetCell(0, col, c)\n}\n\nfunc (t *Table) filtered(data *model1.TableData) *model1.TableData {\n\treturn data.Filter(model1.FilterOpts{\n\t\tToast:  t.toast,\n\t\tFilter: t.cmdBuff.GetText(),\n\t})\n}\n\n// CmdBuff returns the associated command buffer.\nfunc (t *Table) CmdBuff() *model.FishBuff {\n\treturn t.cmdBuff\n}\n\n// ShowDeleted marks row as deleted.\nfunc (t *Table) ShowDeleted() {\n\tr, _ := t.GetSelection()\n\tcols := t.GetColumnCount()\n\tfor x := range cols {\n\t\tt.GetCell(r, x).SetAttributes(tcell.AttrDim)\n\t}\n}\n\n// UpdateTitle refreshes the table title.\nfunc (t *Table) UpdateTitle() {\n\tt.SetTitle(t.styleTitle())\n}\n\nfunc (t *Table) styleTitle() string {\n\trc := int64(t.GetRowCount())\n\tif rc > 0 {\n\t\trc--\n\t}\n\n\tns := t.GetModel().GetNamespace()\n\tif client.IsClusterWide(ns) || ns == client.NotNamespaced {\n\t\tns = client.NamespaceAll\n\t}\n\tpath := t.Path\n\tif path != \"\" {\n\t\tcns, n := client.Namespaced(path)\n\t\tif cns == client.ClusterScope {\n\t\t\tns = n\n\t\t} else {\n\t\t\tns = path\n\t\t}\n\t}\n\tif t.Extras != \"\" {\n\t\tns = t.Extras\n\t}\n\n\tresource := t.gvr.R()\n\tif t.fullGVR {\n\t\tresource = t.gvr.String()\n\t}\n\n\tvar (\n\t\ttitle  string\n\t\tstyles = t.styles.Frame()\n\t)\n\tif ns == client.ClusterScope {\n\t\ttitle = SkinTitle(fmt.Sprintf(TitleFmt, resource, render.AsThousands(rc)), &styles)\n\t} else {\n\t\ttitle = SkinTitle(fmt.Sprintf(NSTitleFmt, resource, ns, render.AsThousands(rc)), &styles)\n\t}\n\n\tbuff := t.cmdBuff.GetText()\n\tif internal.IsLabelSelector(buff) {\n\t\tif sel, err := ExtractLabelSelector(buff); err == nil {\n\t\t\tbuff = render.Truncate(sel.String(), maxTruncate)\n\t\t}\n\t} else if l := t.GetModel().GetLabelSelector(); l != nil && !l.Empty() {\n\t\tbuff = render.Truncate(l.String(), maxTruncate)\n\t} else if buff != \"\" {\n\t\tbuff = render.Truncate(buff, maxTruncate)\n\t}\n\tif buff == \"\" {\n\t\treturn title\n\t}\n\n\treturn title + SkinTitle(fmt.Sprintf(SearchFmt, buff), &styles)\n}\n\n// ROIndicator returns an icon showing whether the session is in readonly mode or not.\nfunc ROIndicator(ro, noIC bool) string {\n\tswitch {\n\tcase noIC:\n\t\treturn \"\"\n\tcase ro:\n\t\treturn lockedIC\n\tdefault:\n\t\treturn unlockedIC\n\t}\n}\n"
  },
  {
    "path": "internal/ui/table_helper.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n)\n\nconst (\n\t// DefaultColorName indicator to keep term colors.\n\tDefaultColorName = \"default\"\n\n\t// SearchFmt represents a filter view title.\n\tSearchFmt = \"<[filter:bg:r]/%s[fg:bg:-]> \"\n\n\t// NSTitleFmt represents a namespaced view title.\n\tNSTitleFmt = \" [fg:bg:b]%s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%s[fg:bg:-]][fg:bg:-] \"\n\n\t// TitleFmt represents a standard view title.\n\tTitleFmt = \" [fg:bg:b]%s[fg:bg:-][[count:bg:b]%s[fg:bg:-]][fg:bg:-] \"\n\n\tdescIndicator = \"↓\"\n\tascIndicator  = \"↑\"\n\n\t// FullFmat specifies a namespaced dump file name.\n\tFullFmat = \"%s-%s-%d.csv\"\n\n\t// NoNSFmat specifies a cluster wide dump file name.\n\tNoNSFmat = \"%s-%d.csv\"\n)\n\nfunc mustExtractStyles(ctx context.Context) *config.Styles {\n\tstyles, ok := ctx.Value(internal.KeyStyles).(*config.Styles)\n\tif !ok {\n\t\tslog.Error(\"Expecting valid styles. Exiting!\")\n\t\tos.Exit(1)\n\t}\n\treturn styles\n}\n\n// TrimCell removes superfluous padding.\nfunc TrimCell(tv *SelectTable, row, col int) string {\n\tc := tv.GetCell(row, col)\n\tif c == nil {\n\t\tslog.Error(\"Trim cell failed\", slogs.Error, fmt.Errorf(\"no cell at [%d:%d]\", row, col))\n\t\treturn \"\"\n\t}\n\treturn strings.TrimSpace(c.Text)\n}\n\n// ExtractLabelSelector extracts label query.\nfunc ExtractLabelSelector(s string) (labels.Selector, error) {\n\tselStr := s\n\tif strings.Index(s, \"-l\") == 0 {\n\t\tselStr = strings.TrimSpace(s[2:])\n\t}\n\n\treturn labels.Parse(selStr)\n}\n\n// SkinTitle decorates a title.\nfunc SkinTitle(fmat string, style *config.Frame) string {\n\tbgColor := style.Title.BgColor\n\tif bgColor == config.DefaultColor {\n\t\tbgColor = config.TransparentColor\n\t}\n\tfmat = strings.ReplaceAll(fmat, \"[fg:bg\", \"[\"+style.Title.FgColor.String()+\":\"+bgColor.String())\n\tfmat = strings.Replace(fmat, \"[hilite\", \"[\"+style.Title.HighlightColor.String(), 1)\n\tfmat = strings.Replace(fmat, \"[key\", \"[\"+style.Menu.NumKeyColor.String(), 1)\n\tfmat = strings.Replace(fmat, \"[filter\", \"[\"+style.Title.FilterColor.String(), 1)\n\tfmat = strings.Replace(fmat, \"[count\", \"[\"+style.Title.CounterColor.String(), 1)\n\tfmat = strings.ReplaceAll(fmat, \":bg:\", \":\"+bgColor.String()+\":\")\n\n\treturn fmat\n}\n\nfunc columnIndicator(sort, selected, asc bool, style *config.Table, name string) string {\n\t// Build the column name with selection indicator\n\tvar displayName string\n\tif selected {\n\t\tdisplayName = fmt.Sprintf(\"[%s::]%s[::]\", style.Header.SelectedSortColumnColor, name)\n\t} else {\n\t\tdisplayName = fmt.Sprintf(\"[%s::]%s[::]\", style.Header.FgColor, name)\n\t}\n\n\t// Add sort indicator if this column is sorted\n\tsuffix := \"\"\n\tif sort {\n\t\torder := descIndicator\n\t\tif asc {\n\t\t\torder = ascIndicator\n\t\t}\n\t\tsuffix = fmt.Sprintf(\"[%s::b]%s[::]\", style.Header.SorterColor, order)\n\t}\n\n\treturn displayName + suffix\n}\n\nfunc formatCell(field string, padding int) string {\n\tif IsASCII(field) {\n\t\treturn Pad(field, padding)\n\t}\n\n\treturn field\n}\n"
  },
  {
    "path": "internal/ui/table_helper_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n)\n\nfunc TestTruncate(t *testing.T) {\n\tuu := map[string]struct {\n\t\ts, e string\n\t}{\n\t\t\"empty\": {},\n\t\t\"max\": {\n\t\t\ts: \"/app.kubernetes.io/instance=prom,app.kubernetes.io/name=prometheus,app.kubernetes.io/component=server\",\n\t\t\te: \"/app.kubernetes.io/instance=prom,app.kubernetes.i…\",\n\t\t},\n\t\t\"less\": {\n\t\t\ts: \"app=fred,env=blee\",\n\t\t\te: \"app=fred,env=blee\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, render.Truncate(u.s, 50))\n\t\t})\n\t}\n}\n\nfunc TestExtractLabelSelector(t *testing.T) {\n\tsel, _ := labels.Parse(\"app=fred,env=blee\")\n\tuu := map[string]struct {\n\t\tsel string\n\t\terr error\n\t\te   labels.Selector\n\t}{\n\t\t\"cool\": {\n\t\t\tsel: \"-l app=fred,env=blee\",\n\t\t\te:   sel,\n\t\t},\n\n\t\t\"no-space\": {\n\t\t\tsel: \"-lapp=fred,env=blee\",\n\t\t\te:   sel,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tsel, err := ExtractLabelSelector(u.sel)\n\t\t\tassert.Equal(t, u.err, err)\n\t\t\tassert.Equal(t, u.e, sel)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/ui/table_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/stretchr/testify/assert\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nfunc TestTableNew(t *testing.T) {\n\tv := ui.NewTable(client.NewGVR(\"fred\"))\n\tv.Init(makeContext())\n\n\tassert.Equal(t, \"fred\", v.GVR().String())\n}\n\nfunc TestTableUpdate(t *testing.T) {\n\tv := ui.NewTable(client.NewGVR(\"fred\"))\n\tv.Init(makeContext())\n\n\tdata := makeTableData()\n\tcdata := v.Update(data, false)\n\tv.UpdateUI(cdata, data)\n\n\tassert.Equal(t, data.RowCount()+1, v.GetRowCount())\n\tassert.Equal(t, data.HeaderCount(), v.GetColumnCount())\n}\n\nfunc TestTableSelection(t *testing.T) {\n\tv := ui.NewTable(client.NewGVR(\"fred\"))\n\tv.Init(makeContext())\n\tm := new(mockModel)\n\tv.SetModel(m)\n\tdata := m.Peek()\n\tcdata := v.Update(data, false)\n\tv.UpdateUI(cdata, data)\n\tv.SelectRow(1, 0, true)\n\n\tr := v.GetSelectedRow(\"r1\")\n\tif r != nil {\n\t\tassert.Equal(t, model1.Row{ID: \"r1\", Fields: model1.Fields{\"blee\", \"duh\", \"fred\"}}, *r)\n\t}\n\tassert.Equal(t, \"r1\", v.GetSelectedItem())\n\tassert.Equal(t, \"blee\", v.GetSelectedCell(0))\n\tassert.Equal(t, 1, v.GetSelectedRowIndex())\n\tassert.Equal(t, []string{\"r1\"}, v.GetSelectedItems())\n\n\tv.ClearSelection()\n\tv.SelectFirstRow()\n\tassert.Equal(t, 1, v.GetSelectedRowIndex())\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\ntype mockModel struct{}\n\nvar _ ui.Tabular = &mockModel{}\n\nfunc (*mockModel) SetViewSetting(context.Context, *config.ViewSetting) {}\nfunc (*mockModel) SetInstance(string)                                  {}\nfunc (*mockModel) SetLabelSelector(labels.Selector)                    {}\nfunc (*mockModel) GetLabelSelector() labels.Selector                   { return nil }\nfunc (*mockModel) Empty() bool                                         { return false }\nfunc (*mockModel) RowCount() int                                       { return 1 }\nfunc (*mockModel) HasMetrics() bool                                    { return true }\nfunc (*mockModel) Peek() *model1.TableData                             { return makeTableData() }\nfunc (*mockModel) Refresh(context.Context) error                       { return nil }\nfunc (*mockModel) ClusterWide() bool                                   { return false }\nfunc (*mockModel) GetNamespace() string                                { return \"blee\" }\nfunc (*mockModel) SetNamespace(string)                                 {}\nfunc (*mockModel) ToggleToast()                                        {}\nfunc (*mockModel) AddListener(model.TableListener)                     {}\nfunc (*mockModel) RemoveListener(model.TableListener)                  {}\nfunc (*mockModel) Watch(context.Context) error                         { return nil }\nfunc (*mockModel) Get(context.Context, string) (runtime.Object, error) { return nil, nil }\nfunc (*mockModel) InNamespace(string) bool                             { return true }\nfunc (*mockModel) SetRefreshRate(time.Duration)                        {}\n\nfunc (*mockModel) Delete(context.Context, string, *metav1.DeletionPropagation, dao.Grace) error {\n\treturn nil\n}\n\nfunc (*mockModel) Describe(context.Context, string) (string, error) {\n\treturn \"\", nil\n}\n\nfunc (*mockModel) ToYAML(context.Context, string) (string, error) {\n\treturn \"\", nil\n}\n\nfunc makeTableData() *model1.TableData {\n\treturn model1.NewTableDataWithRows(\n\t\tclient.NewGVR(\"test\"),\n\t\tmodel1.Header{\n\t\t\tmodel1.HeaderColumn{Name: \"A\"},\n\t\t\tmodel1.HeaderColumn{Name: \"B\"},\n\t\t\tmodel1.HeaderColumn{Name: \"C\"},\n\t\t},\n\t\tmodel1.NewRowEventsWithEvts(\n\t\t\tmodel1.RowEvent{\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tID:     \"r1\",\n\t\t\t\t\tFields: model1.Fields{\"blee\", \"duh\", \"fred\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodel1.RowEvent{\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tID:     \"r2\",\n\t\t\t\t\tFields: model1.Fields{\"blee\", \"duh\", \"zorg\"},\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t)\n}\n\nfunc makeContext() context.Context {\n\tctx := context.WithValue(context.Background(), internal.KeyStyles, config.NewStyles())\n\tctx = context.WithValue(ctx, internal.KeyViewConfig, config.NewCustomView())\n\n\treturn ctx\n}\n"
  },
  {
    "path": "internal/ui/tree.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"context\"\n\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n)\n\n// KeyListenerFunc listens to key presses.\ntype KeyListenerFunc func()\n\n// Tree represents a tree view.\ntype Tree struct {\n\t*tview.TreeView\n\n\tactions      *KeyActions\n\tselectedItem string\n\tcmdBuff      *model.FishBuff\n\texpandNodes  bool\n\tCount        int\n\tkeyListener  KeyListenerFunc\n}\n\n// NewTree returns a new view.\nfunc NewTree() *Tree {\n\treturn &Tree{\n\t\tTreeView:    tview.NewTreeView(),\n\t\texpandNodes: true,\n\t\tactions:     NewKeyActions(),\n\t\tcmdBuff:     model.NewFishBuff('/', model.FilterBuffer),\n\t}\n}\n\n// Init initializes the view.\nfunc (t *Tree) Init(context.Context) error {\n\tt.BindKeys()\n\tt.SetBorder(true)\n\tt.SetBorderAttributes(tcell.AttrBold)\n\tt.SetBorderPadding(0, 0, 1, 1)\n\tt.SetGraphics(true)\n\tt.SetGraphicsColor(tcell.ColorCadetBlue)\n\tt.SetInputCapture(t.keyboard)\n\n\treturn nil\n}\n\n// SetSelectedItem sets the currently selected node.\nfunc (t *Tree) SetSelectedItem(s string) {\n\tt.selectedItem = s\n}\n\n// GetSelectedItem returns the currently selected item or blank if none.\nfunc (t *Tree) GetSelectedItem() string {\n\treturn t.selectedItem\n}\n\n// ExpandNodes returns true if nodes are expanded or false otherwise.\nfunc (t *Tree) ExpandNodes() bool {\n\treturn t.expandNodes\n}\n\n// CmdBuff returns the filter command.\nfunc (t *Tree) CmdBuff() *model.FishBuff {\n\treturn t.cmdBuff\n}\n\n// SetKeyListenerFn sets a key entered listener.\nfunc (t *Tree) SetKeyListenerFn(f KeyListenerFunc) {\n\tt.keyListener = f\n}\n\n// Actions returns active menu bindings.\nfunc (t *Tree) Actions() *KeyActions {\n\treturn t.actions\n}\n\n// Hints returns the view hints.\nfunc (t *Tree) Hints() model.MenuHints {\n\treturn t.actions.Hints()\n}\n\n// ExtraHints returns additional hints.\nfunc (*Tree) ExtraHints() map[string]string {\n\treturn nil\n}\n\n// BindKeys binds default mnemonics.\nfunc (t *Tree) BindKeys() {\n\tt.Actions().Merge(NewKeyActionsFromMap(KeyMap{\n\t\tKeySpace: NewKeyAction(\"Expand/Collapse\", t.noopCmd, true),\n\t\tKeyX:     NewKeyAction(\"Expand/Collapse All\", t.toggleCollapseCmd, true),\n\t}))\n}\n\nfunc (t *Tree) keyboard(evt *tcell.EventKey) *tcell.EventKey {\n\tif a, ok := t.actions.Get(AsKey(evt)); ok {\n\t\treturn a.Action(evt)\n\t}\n\n\treturn evt\n}\n\nfunc (*Tree) noopCmd(evt *tcell.EventKey) *tcell.EventKey {\n\treturn evt\n}\n\nfunc (t *Tree) toggleCollapseCmd(*tcell.EventKey) *tcell.EventKey {\n\tt.expandNodes = !t.expandNodes\n\tt.GetRoot().Walk(func(node, parent *tview.TreeNode) bool {\n\t\tif parent != nil {\n\t\t\tnode.SetExpanded(t.expandNodes)\n\t\t}\n\t\treturn true\n\t})\n\treturn nil\n}\n\n// ClearSelection clears the currently selected node.\nfunc (t *Tree) ClearSelection() {\n\tt.selectedItem = \"\"\n\tt.SetCurrentNode(nil)\n}\n"
  },
  {
    "path": "internal/ui/types.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage ui\n\nimport (\n\t\"context\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nconst (\n\tunlockedIC = \"[RW]\"\n\tlockedIC   = \"[R]\"\n)\n\n// Namespaceable tracks namespaces.\ntype Namespaceable interface {\n\t// ClusterWide returns true if the model represents resource in all namespaces.\n\tClusterWide() bool\n\n\t// GetNamespace returns the model namespace.\n\tGetNamespace() string\n\n\t// SetNamespace changes the model namespace.\n\tSetNamespace(string)\n\n\t// InNamespace check if current namespace matches models.\n\tInNamespace(string) bool\n}\n\n// Lister tracks resource getter.\ntype Lister interface {\n\t// Get returns a resource instance.\n\tGet(ctx context.Context, path string) (runtime.Object, error)\n}\n\n// Tabular represents a tabular model.\ntype Tabular interface {\n\tNamespaceable\n\tLister\n\n\t// SetInstance sets parent resource path.\n\tSetInstance(string)\n\n\t// SetLabelSelector sets the label selector.\n\tSetLabelSelector(labels.Selector)\n\n\t// GetLabelSelector fetch the label filter.\n\tGetLabelSelector() labels.Selector\n\n\t// Empty returns true if model has no data.\n\tEmpty() bool\n\n\t// RowCount returns the model data count.\n\tRowCount() int\n\n\t// Peek returns current model data.\n\tPeek() *model1.TableData\n\n\t// Watch watches a given resource for changes.\n\tWatch(context.Context) error\n\n\t// Refresh forces a new refresh.\n\tRefresh(context.Context) error\n\n\t// SetRefreshRate sets the model watch loop rate.\n\tSetRefreshRate(time.Duration)\n\n\t// AddListener registers a model listener.\n\tAddListener(model.TableListener)\n\n\t// RemoveListener unregister a model listener.\n\tRemoveListener(model.TableListener)\n\n\t// Delete a resource.\n\tDelete(context.Context, string, *metav1.DeletionPropagation, dao.Grace) error\n\n\t// SetViewSetting injects custom cols specification.\n\tSetViewSetting(context.Context, *config.ViewSetting)\n}\n"
  },
  {
    "path": "internal/view/actions.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/ui/dialog\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"k8s.io/apimachinery/pkg/util/sets\"\n)\n\n// AllScopes represents actions available for all views.\nconst AllScopes = \"all\"\n\n// Runner represents a runnable action handler.\ntype Runner interface {\n\t// App returns the current app.\n\tApp() *App\n\n\t// GetSelectedItem returns the current selected item.\n\tGetSelectedItem() string\n\n\t// Aliases returns all aliases assoxciated with the view GVR.\n\tAliases() sets.Set[string]\n\n\t// EnvFn returns the current environment function.\n\tEnvFn() EnvFunc\n}\n\nfunc hasAll(scopes []string) bool {\n\treturn slices.Contains(scopes, AllScopes)\n}\n\nfunc includes(aliases []string, s string) bool {\n\treturn slices.Contains(aliases, s)\n}\n\nfunc inScope(scopes []string, aliases sets.Set[string]) bool {\n\tif hasAll(scopes) {\n\t\treturn true\n\t}\n\tfor _, s := range scopes {\n\t\tif _, ok := aliases[s]; ok {\n\t\t\treturn ok\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc hotKeyActions(r Runner, aa *ui.KeyActions) error {\n\thh := config.NewHotKeys()\n\taa.Range(func(k tcell.Key, a ui.KeyAction) {\n\t\tif a.Opts.HotKey {\n\t\t\taa.Delete(k)\n\t\t}\n\t})\n\n\tvar errs error\n\tif err := hh.Load(r.App().Config.ContextHotkeysPath()); err != nil {\n\t\terrs = errors.Join(errs, err)\n\t}\n\tfor k, hk := range hh.HotKey {\n\t\tkey, err := asKey(hk.ShortCut)\n\t\tif err != nil {\n\t\t\terrs = errors.Join(errs, err)\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := aa.Get(key); ok {\n\t\t\tif !hk.Override {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"duplicate hotkey found for %q in %q\", hk.ShortCut, k))\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tslog.Debug(\"HotKey overrode action shortcut\",\n\t\t\t\tslogs.Shortcut, hk.ShortCut,\n\t\t\t\tslogs.Key, k,\n\t\t\t)\n\t\t}\n\n\t\tcommand, err := r.EnvFn()().Substitute(hk.Command)\n\t\tif err != nil {\n\t\t\tslog.Warn(\"Invalid shortcut command\", slogs.Error, err)\n\t\t\tcontinue\n\t\t}\n\n\t\taa.Add(key, ui.NewKeyActionWithOpts(\n\t\t\thk.Description,\n\t\t\tgotoCmd(r, command, \"\", !hk.KeepHistory),\n\t\t\tui.ActionOpts{\n\t\t\t\tShared: true,\n\t\t\t\tHotKey: true,\n\t\t\t},\n\t\t))\n\t}\n\n\treturn errs\n}\n\nfunc gotoCmd(r Runner, cmd, path string, clearStack bool) ui.ActionHandler {\n\treturn func(*tcell.EventKey) *tcell.EventKey {\n\t\tr.App().gotoResource(cmd, path, clearStack, true)\n\t\treturn nil\n\t}\n}\n\nfunc pluginActions(r Runner, aa *ui.KeyActions) error {\n\t// Skip plugin loading if no valid connection\n\tif r.App().Conn() == nil || !r.App().Conn().ConnectionOK() {\n\t\treturn nil\n\t}\n\n\taa.Range(func(k tcell.Key, a ui.KeyAction) {\n\t\tif a.Opts.Plugin {\n\t\t\taa.Delete(k)\n\t\t}\n\t})\n\n\tpath, err := r.App().Config.ContextPluginsPath()\n\tif err != nil {\n\t\treturn err\n\t}\n\tpp := config.NewPlugins()\n\tif err := pp.Load(path, true); err != nil {\n\t\treturn err\n\t}\n\n\tvar (\n\t\terrs    error\n\t\taliases = r.Aliases()\n\t\tro      = r.App().Config.IsReadOnly()\n\t)\n\tfor k := range pp.Plugins {\n\t\tif !inScope(pp.Plugins[k].Scopes, aliases) || (ro && pp.Plugins[k].Dangerous) {\n\t\t\tcontinue\n\t\t}\n\t\tkey, err := asKey(pp.Plugins[k].ShortCut)\n\t\tif err != nil {\n\t\t\terrs = errors.Join(errs, err)\n\t\t\tcontinue\n\t\t}\n\t\tif _, ok := aa.Get(key); ok {\n\t\t\tif !pp.Plugins[k].Override {\n\t\t\t\terrs = errors.Join(errs, fmt.Errorf(\"duplicate plugin key found for %q in %q\", pp.Plugins[k].ShortCut, k))\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tslog.Debug(\"Plugin overrode action shortcut\",\n\t\t\t\tslogs.Plugin, k,\n\t\t\t\tslogs.Key, pp.Plugins[k].ShortCut,\n\t\t\t)\n\t\t}\n\n\t\tplugin := pp.Plugins[k]\n\t\taa.Add(key, ui.NewKeyActionWithOpts(\n\t\t\tpp.Plugins[k].Description,\n\t\t\tpluginAction(r, &plugin),\n\t\t\tui.ActionOpts{\n\t\t\t\tVisible:   true,\n\t\t\t\tPlugin:    true,\n\t\t\t\tDangerous: plugin.Dangerous,\n\t\t\t},\n\t\t))\n\t}\n\n\treturn errs\n}\n\nfunc pluginAction(r Runner, p *config.Plugin) ui.ActionHandler {\n\treturn func(evt *tcell.EventKey) *tcell.EventKey {\n\t\tpath := r.GetSelectedItem()\n\t\tif path == \"\" {\n\t\t\treturn evt\n\t\t}\n\t\tif r.EnvFn() == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\t// Collect inputs if defined, then execute plugin\n\t\tif len(p.Inputs) > 0 {\n\t\t\td := r.App().Styles.Dialog()\n\t\t\tdialog.ShowPluginInputs(&d, r.App().Content.Pages, \"Plugin Inputs\", p.Inputs,\n\t\t\t\tfunc(msg string) {\n\t\t\t\t\tr.App().Flash().Warn(msg)\n\t\t\t\t},\n\t\t\t\tfunc(inputValues dialog.PluginInputValues) {\n\t\t\t\t\texecutePlugin(r, p, inputValues)\n\t\t\t\t},\n\t\t\t\tfunc() {},\n\t\t\t)\n\t\t\treturn nil\n\t\t}\n\n\t\texecutePlugin(r, p, nil)\n\t\treturn nil\n\t}\n}\n\nfunc executePlugin(r Runner, p *config.Plugin, inputValues dialog.PluginInputValues) {\n\t// Get base environment and add input values with INPUT_ prefix\n\tenv := r.EnvFn()()\n\tfor name, value := range inputValues {\n\t\tenv[\"INPUT_\"+strings.ToUpper(name)] = value\n\t}\n\n\targs := make([]string, len(p.Args))\n\tfor i, a := range p.Args {\n\t\targ, err := env.Substitute(a)\n\t\tif err != nil {\n\t\t\tslog.Error(\"Plugin Args match failed\", slogs.Error, err)\n\t\t\treturn\n\t\t}\n\t\targs[i] = arg\n\t}\n\n\tcb := func() {\n\t\topts := shellOpts{\n\t\t\tbinary:     p.Command,\n\t\t\tbackground: p.Background,\n\t\t\tpipes:      p.Pipes,\n\t\t\targs:       args,\n\t\t}\n\t\tsuspend, errChan, statusChan := run(r.App(), &opts)\n\t\tif !suspend {\n\t\t\tr.App().Flash().Infof(\"Plugin command failed: %q\", p.Description)\n\t\t\treturn\n\t\t}\n\t\tvar errs error\n\t\tfor e := range errChan {\n\t\t\terrs = errors.Join(errs, e)\n\t\t}\n\t\tif errs != nil {\n\t\t\tif !strings.Contains(errs.Error(), \"signal: interrupt\") {\n\t\t\t\tslog.Error(\"Plugin command failed\", slogs.Error, errs)\n\t\t\t\tr.App().cowCmd(errs.Error())\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tgo func() {\n\t\t\tfor st := range statusChan {\n\t\t\t\tif !p.OverwriteOutput {\n\t\t\t\t\tr.App().Flash().Infof(\"Plugin command launched successfully: %q\", st)\n\t\t\t\t} else if strings.Contains(st, outputPrefix) {\n\t\t\t\t\tinfoMsg := strings.TrimPrefix(st, outputPrefix)\n\t\t\t\t\tr.App().Flash().Info(strings.TrimSpace(infoMsg))\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\tif p.ShouldConfirm() {\n\t\tmsg := fmt.Sprintf(\"Run?\\n%s %s\", p.Command, strings.Join(args, \" \"))\n\t\td := r.App().Styles.Dialog()\n\t\tdialog.ShowConfirm(&d, r.App().Content.Pages, \"Confirm \"+p.Description, msg, cb, func() {})\n\t\treturn\n\t}\n\tcb()\n}\n"
  },
  {
    "path": "internal/view/actions_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"k8s.io/apimachinery/pkg/util/sets\"\n)\n\nfunc init() {\n\tslog.SetDefault(slog.New(slog.DiscardHandler))\n}\n\nfunc TestHasAll(t *testing.T) {\n\tuu := map[string]struct {\n\t\tscopes []string\n\t\te      bool\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"all\": {\n\t\t\tscopes: []string{\"blee\", \"duh\", AllScopes},\n\t\t\te:      true,\n\t\t},\n\n\t\t\"none\": {\n\t\t\tscopes: []string{\"blee\", \"duh\", \"alla\"},\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, hasAll(u.scopes))\n\t\t})\n\t}\n}\n\nfunc TestIncludes(t *testing.T) {\n\tuu := map[string]struct {\n\t\ts  string\n\t\tss []string\n\t\te  bool\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"yes\": {\n\t\t\ts:  \"blee\",\n\t\t\tss: []string{\"yo\", \"duh\", \"blee\"},\n\t\t\te:  true,\n\t\t},\n\n\t\t\"no\": {\n\t\t\ts:  \"blue\",\n\t\t\tss: []string{\"yo\", \"duh\", \"blee\"},\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, includes(u.ss, u.s))\n\t\t})\n\t}\n}\n\nfunc TestInScope(t *testing.T) {\n\tuu := map[string]struct {\n\t\tss []string\n\t\taa sets.Set[string]\n\t\te  bool\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"yes\": {\n\t\t\te:  true,\n\t\t\tss: []string{\"blee\", \"duh\", \"fred\"},\n\t\t\taa: sets.New(\"blee\", \"fred\", \"duh\"),\n\t\t},\n\n\t\t\"no\": {\n\t\t\tss: []string{\"blee\", \"duh\", \"fred\"},\n\t\t\taa: sets.New(\"blee1\", \"fred1\"),\n\t\t},\n\n\t\t\"no-scopes\": {\n\t\t\taa: sets.New(\"aa\", \"blee1\", \"fred1\"),\n\t\t},\n\n\t\t\"no-aliases\": {\n\t\t\tss: []string{\"blee1\", \"fred1\"},\n\t\t},\n\n\t\t\"all\": {\n\t\t\te:  true,\n\t\t\tss: []string{AllScopes},\n\t\t\taa: sets.New(\"blee1\", \"fred1\"),\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, inScope(u.ss, u.aa))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/view/alias.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\nconst aliasTitle = \"Aliases\"\n\n// Alias represents a command alias view.\ntype Alias struct {\n\tResourceViewer\n}\n\n// NewAlias returns a new alias view.\nfunc NewAlias(gvr *client.GVR) ResourceViewer {\n\ta := Alias{\n\t\tResourceViewer: NewBrowser(gvr),\n\t}\n\ta.GetTable().SetBorderFocusColor(tcell.ColorAliceBlue)\n\ta.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorAliceBlue).Attributes(tcell.AttrNone))\n\ta.AddBindKeysFn(a.bindKeys)\n\ta.SetContextFn(a.aliasContext)\n\n\treturn &a\n}\n\n// Init initializes the view.\nfunc (a *Alias) Init(ctx context.Context) error {\n\tif err := a.ResourceViewer.Init(ctx); err != nil {\n\t\treturn err\n\t}\n\ta.GetTable().GetModel().SetNamespace(client.NotNamespaced)\n\n\treturn nil\n}\n\nfunc (a *Alias) aliasContext(ctx context.Context) context.Context {\n\treturn context.WithValue(ctx, internal.KeyAliases, a.App().command.alias)\n}\n\nfunc (a *Alias) bindKeys(aa *ui.KeyActions) {\n\taa.Delete(ui.KeyShiftA, ui.KeyShiftN, ui.KeyShiftS, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace)\n\taa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL)\n\taa.Bulk(ui.KeyMap{\n\t\ttcell.KeyEnter: ui.NewKeyAction(\"Goto\", a.gotoCmd, true),\n\t\tui.KeyShiftR:   ui.NewKeyAction(\"Sort Resource\", a.GetTable().SortColCmd(\"RESOURCE\", true), false),\n\t\tui.KeyShiftC:   ui.NewKeyAction(\"Sort Command\", a.GetTable().SortColCmd(\"COMMAND\", true), false),\n\t\tui.KeyShiftA:   ui.NewKeyAction(\"Sort ApiGroup\", a.GetTable().SortColCmd(\"API-GROUP\", true), false),\n\t})\n}\n\nfunc (a *Alias) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif a.GetTable().CmdBuff().IsActive() {\n\t\treturn a.GetTable().activateCmd(evt)\n\t}\n\n\tpath := a.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\ta.App().gotoResource(client.NewGVR(path).String(), \"\", true, true)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/view/alias_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/config/mock\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nfunc TestAliasNew(t *testing.T) {\n\tv := view.NewAlias(client.AliGVR)\n\n\trequire.NoError(t, v.Init(makeContext(t)))\n\tassert.Equal(t, \"Aliases\", v.Name())\n\tassert.Len(t, v.Hints(), 7)\n}\n\nfunc TestAliasSearch(t *testing.T) {\n\tv := view.NewAlias(client.AliGVR)\n\trequire.NoError(t, v.Init(makeContext(t)))\n\tv.GetTable().SetModel(new(mockModel))\n\tv.GetTable().Refresh()\n\tv.App().Prompt().SetModel(v.GetTable().CmdBuff())\n\tv.App().Prompt().SendStrokes(\"blee\")\n\n\tassert.Equal(t, 3, v.GetTable().GetColumnCount())\n\tassert.Equal(t, 3, v.GetTable().GetRowCount())\n}\n\nfunc TestAliasGoto(t *testing.T) {\n\tv := view.NewAlias(client.AliGVR)\n\trequire.NoError(t, v.Init(makeContext(t)))\n\tv.GetTable().Select(0, 0)\n\n\tb := buffL{}\n\tv.GetTable().CmdBuff().SetActive(true)\n\tv.GetTable().CmdBuff().AddListener(&b)\n\tv.GetTable().SendKey(tcell.NewEventKey(tcell.KeyEnter, 256, tcell.ModNone))\n\n\tassert.True(t, v.GetTable().CmdBuff().IsActive())\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\ntype buffL struct {\n\tactive  int\n\tchanged int\n}\n\nfunc (b *buffL) BufferChanged(string, string) {\n\tb.changed++\n}\nfunc (*buffL) BufferCompleted(string, string) {}\n\nfunc (b *buffL) BufferActive(bool, model.BufferKind) {\n\tb.active++\n}\n\nfunc makeContext(t testing.TB) context.Context {\n\ta := view.NewApp(mock.NewMockConfig(t))\n\tctx := context.WithValue(context.Background(), internal.KeyApp, a)\n\treturn context.WithValue(ctx, internal.KeyStyles, a.Styles)\n}\n\ntype mockModel struct{}\n\nvar (\n\t_ ui.Tabular   = (*mockModel)(nil)\n\t_ ui.Suggester = (*mockModel)(nil)\n)\n\nfunc (*mockModel) SetViewSetting(context.Context, *config.ViewSetting) {}\nfunc (*mockModel) CurrentSuggestion() (string, bool)                   { return \"\", false }\nfunc (*mockModel) NextSuggestion() (string, bool)                      { return \"\", false }\nfunc (*mockModel) PrevSuggestion() (string, bool)                      { return \"\", false }\nfunc (*mockModel) ClearSuggestions()                                   {}\nfunc (*mockModel) SetInstance(string)                                  {}\nfunc (*mockModel) SetLabelSelector(labels.Selector)                    {}\nfunc (*mockModel) GetLabelSelector() labels.Selector                   { return nil }\nfunc (*mockModel) Empty() bool                                         { return false }\nfunc (*mockModel) RowCount() int                                       { return 1 }\nfunc (*mockModel) HasMetrics() bool                                    { return true }\nfunc (*mockModel) Peek() *model1.TableData                             { return makeTableData() }\nfunc (*mockModel) ClusterWide() bool                                   { return false }\nfunc (*mockModel) GetNamespace() string                                { return \"blee\" }\nfunc (*mockModel) SetNamespace(string)                                 {}\nfunc (*mockModel) ToggleToast()                                        {}\nfunc (*mockModel) AddListener(model.TableListener)                     {}\nfunc (*mockModel) RemoveListener(model.TableListener)                  {}\nfunc (*mockModel) Watch(context.Context) error                         { return nil }\nfunc (*mockModel) Refresh(context.Context) error                       { return nil }\nfunc (*mockModel) Get(context.Context, string) (runtime.Object, error) {\n\treturn nil, nil\n}\n\nfunc (*mockModel) Delete(context.Context, string, *metav1.DeletionPropagation, dao.Grace) error {\n\treturn nil\n}\n\nfunc (*mockModel) Describe(context.Context, string) (string, error) {\n\treturn \"\", nil\n}\n\nfunc (*mockModel) ToYAML(context.Context, string) (string, error) {\n\treturn \"\", nil\n}\n\nfunc (*mockModel) InNamespace(string) bool      { return true }\nfunc (*mockModel) SetRefreshRate(time.Duration) {}\n\nfunc makeTableData() *model1.TableData {\n\treturn model1.NewTableDataWithRows(\n\t\tclient.NewGVR(\"test\"),\n\t\tmodel1.Header{\n\t\t\tmodel1.HeaderColumn{Name: \"RESOURCE\"},\n\t\t\tmodel1.HeaderColumn{Name: \"COMMAND\"},\n\t\t\tmodel1.HeaderColumn{Name: \"APIGROUP\"},\n\t\t},\n\t\tmodel1.NewRowEventsWithEvts(\n\t\t\tmodel1.RowEvent{\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tID:     \"r1\",\n\t\t\t\t\tFields: model1.Fields{\"blee\", \"duh\", \"fred\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodel1.RowEvent{\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tID:     \"r2\",\n\t\t\t\t\tFields: model1.Fields{\"fred\", \"duh\", \"zorg\"},\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t)\n}\n"
  },
  {
    "path": "internal/view/app.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"maps\"\n\t\"os\"\n\t\"os/signal\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/cenkalti/backoff/v4\"\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/ui/dialog\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n\t\"github.com/derailed/k9s/internal/vul\"\n\t\"github.com/derailed/k9s/internal/watch\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n)\n\n// ExitStatus indicates UI exit conditions.\nvar ExitStatus = \"\"\n\nconst (\n\tsplashDelay      = 1 * time.Second\n\tclusterRefresh   = 15 * time.Second\n\tclusterInfoWidth = 50\n\tclusterInfoPad   = 15\n)\n\n// App represents an application view.\ntype App struct {\n\tversion string\n\t*ui.App\n\tContent       *PageStack\n\tcommand       *Command\n\tfactory       *watch.Factory\n\tcancelFn      context.CancelFunc\n\tclusterModel  *model.ClusterInfo\n\tcmdHistory    *model.History\n\tfilterHistory *model.History\n\tconRetry      int32\n\tshowHeader    bool\n\tshowLogo      bool\n\tshowCrumbs    bool\n}\n\n// NewApp returns a K9s app instance.\nfunc NewApp(cfg *config.Config) *App {\n\ta := App{\n\t\tApp:           ui.NewApp(cfg, cfg.K9s.ActiveContextName()),\n\t\tcmdHistory:    model.NewHistory(model.MaxHistory),\n\t\tfilterHistory: model.NewHistory(model.MaxHistory),\n\t\tContent:       NewPageStack(),\n\t}\n\ta.ReloadStyles()\n\n\ta.Views()[\"statusIndicator\"] = ui.NewStatusIndicator(a.App, a.Styles)\n\ta.Views()[\"clusterInfo\"] = NewClusterInfo(&a)\n\n\treturn &a\n}\n\n// ReloadStyles reloads skin file.\nfunc (a *App) ReloadStyles() {\n\ta.RefreshStyles(a)\n}\n\n// UpdateClusterInfo updates clusterInfo panel\nfunc (a *App) UpdateClusterInfo() {\n\tif a.factory != nil {\n\t\ta.clusterModel.Reset(a.factory)\n\t}\n}\n\n// ConOK checks the connection is cool, returns false otherwise.\nfunc (a *App) ConOK() bool {\n\treturn atomic.LoadInt32(&a.conRetry) == 0\n}\n\n// Init initializes the application.\nfunc (a *App) Init(version string, _ int) error {\n\ta.version = model.NormalizeVersion(version)\n\n\tctx := context.WithValue(context.Background(), internal.KeyApp, a)\n\tif err := a.Content.Init(ctx); err != nil {\n\t\treturn err\n\t}\n\ta.Content.AddListener(a.Crumbs())\n\ta.Content.AddListener(a.Menu())\n\n\ta.App.Init()\n\ta.SetInputCapture(a.keyboard)\n\ta.bindKeys()\n\n\t// Allow initialization even without a valid connection\n\t// We'll fall back to context view in defaultCmd\n\tif a.Conn() != nil {\n\t\tns := a.Config.ActiveNamespace()\n\t\ta.factory = watch.NewFactory(a.Conn())\n\t\ta.initFactory(ns)\n\n\t\ta.clusterModel = model.NewClusterInfo(a.factory, a.version, a.Config.K9s)\n\t\ta.clusterModel.AddListener(a.clusterInfo())\n\t\ta.clusterModel.AddListener(a.statusIndicator())\n\t\tif a.Conn().ConnectionOK() {\n\t\t\tgo func() {\n\t\t\t\ta.clusterModel.Refresh()\n\t\t\t\ta.QueueUpdateDraw(func() {\n\t\t\t\t\ta.clusterInfo().Init()\n\t\t\t\t})\n\t\t\t}()\n\t\t}\n\t}\n\n\ta.command = NewCommand(a)\n\tif err := a.command.Init(a.Config.ContextAliasesPath()); err != nil {\n\t\treturn err\n\t}\n\ta.CmdBuff().SetSuggestionFn(a.suggestCommand())\n\n\ta.layout(ctx)\n\ta.initSignals()\n\n\tif a.Config.K9s.ImageScans.Enable {\n\t\ta.initImgScanner(version)\n\t}\n\ta.ReloadStyles()\n\n\treturn nil\n}\n\nfunc (*App) stopImgScanner() {\n\tif vul.ImgScanner != nil {\n\t\tvul.ImgScanner.Stop()\n\t}\n}\n\nfunc (a *App) clearHistory() {\n\ta.cmdHistory.Clear()\n\ta.filterHistory.Clear()\n}\n\nfunc (a *App) initImgScanner(version string) {\n\tdefer func(t time.Time) {\n\t\tslog.Debug(\"Scanner init time\", slogs.Elapsed, time.Since(t))\n\t}(time.Now())\n\n\tvul.ImgScanner = vul.NewImageScanner(a.Config.K9s.ImageScans, slog.Default())\n\tgo vul.ImgScanner.Init(\"k9s\", version)\n}\n\nfunc (a *App) layout(ctx context.Context) {\n\tflash := ui.NewFlash(a.App)\n\tgo flash.Watch(ctx, a.Flash().Channel())\n\n\tmain := tview.NewFlex().SetDirection(tview.FlexRow)\n\tmain.AddItem(a.statusIndicator(), 1, 1, false)\n\tmain.AddItem(a.Content, 0, 10, true)\n\tif !a.Config.K9s.IsCrumbsless() {\n\t\tmain.AddItem(a.Crumbs(), 1, 1, false)\n\t}\n\tmain.AddItem(flash, 1, 1, false)\n\n\ta.Main.AddPage(\"main\", main, true, false)\n\ta.toggleHeader(!a.Config.K9s.IsHeadless(), !a.Config.K9s.IsLogoless())\n\tif !a.Config.K9s.IsSplashless() {\n\t\ta.Main.AddPage(\"splash\", ui.NewSplash(a.Styles, a.version), true, true)\n\t}\n}\n\nfunc (*App) initSignals() {\n\tsig := make(chan os.Signal, 1)\n\tsignal.Notify(sig, syscall.SIGHUP)\n\n\tgo func(sig chan os.Signal) {\n\t\t<-sig\n\t\tos.Exit(0)\n\t}(sig)\n}\n\nfunc (a *App) suggestCommand() model.SuggestionFunc {\n\tcontextNames, err := a.contextNames()\n\tif err != nil {\n\t\tslog.Error(\"Failed to list contexts\", slogs.Error, err)\n\t}\n\n\treturn func(s string) (entries sort.StringSlice) {\n\t\tif s == \"\" {\n\t\t\tif a.cmdHistory.Empty() {\n\t\t\t\treturn\n\t\t\t}\n\t\t\treturn a.cmdHistory.List()\n\t\t}\n\n\t\tls := strings.ToLower(s)\n\t\tfor alias := range maps.Keys(a.command.alias.Alias) {\n\t\t\tif suggest, ok := cmd.ShouldAddSuggest(ls, alias); ok {\n\t\t\t\tentries = append(entries, suggest)\n\t\t\t}\n\t\t}\n\n\t\tnamespaceNames, err := a.factory.Client().ValidNamespaceNames()\n\t\tif err != nil {\n\t\t\tslog.Error(\"Failed to obtain list of namespaces\", slogs.Error, err)\n\t\t}\n\t\tentries = append(entries, cmd.SuggestSubCommand(s, namespaceNames, contextNames)...)\n\t\tif len(entries) == 0 {\n\t\t\treturn nil\n\t\t}\n\t\tentries.Sort()\n\t\treturn\n\t}\n}\n\nfunc (a *App) contextNames() ([]string, error) {\n\t// Return empty list if no factory\n\tif a.factory == nil {\n\t\treturn []string{}, nil\n\t}\n\tcontexts, err := a.factory.Client().Config().Contexts()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcontextNames := make([]string, 0, len(contexts))\n\tfor ctxName := range contexts {\n\t\tcontextNames = append(contextNames, ctxName)\n\t}\n\n\treturn contextNames, nil\n}\n\nfunc (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey {\n\tif k, ok := a.HasAction(ui.AsKey(evt)); ok && !a.Content.IsTopDialog() {\n\t\treturn k.Action(evt)\n\t}\n\n\treturn evt\n}\n\nfunc (a *App) bindKeys() {\n\ta.AddActions(ui.NewKeyActionsFromMap(ui.KeyMap{\n\t\ttcell.KeyCtrlE:     ui.NewSharedKeyAction(\"ToggleHeader\", a.toggleHeaderCmd, false),\n\t\ttcell.KeyCtrlG:     ui.NewSharedKeyAction(\"ToggleCrumbs\", a.toggleCrumbsCmd, false),\n\t\tui.KeyHelp:         ui.NewSharedKeyAction(\"Help\", a.helpCmd, false),\n\t\tui.KeyLeftBracket:  ui.NewSharedKeyAction(\"Go Back\", a.previousCommand, false),\n\t\tui.KeyRightBracket: ui.NewSharedKeyAction(\"Go Forward\", a.nextCommand, false),\n\t\tui.KeyDash:         ui.NewSharedKeyAction(\"Last View\", a.lastCommand, false),\n\t\ttcell.KeyCtrlA:     ui.NewSharedKeyAction(\"Aliases\", a.aliasCmd, false),\n\t\ttcell.KeyEnter:     ui.NewKeyAction(\"Goto\", a.gotoCmd, false),\n\t\ttcell.KeyCtrlC:     ui.NewKeyAction(\"Quit\", a.quitCmd, false),\n\t}))\n}\n\n// ActiveView returns the currently active view.\nfunc (a *App) ActiveView() model.Component {\n\treturn a.Content.GetPrimitive(\"main\").(model.Component)\n}\n\nfunc (a *App) toggleHeader(header, logo bool) {\n\ta.showHeader, a.showLogo = header, logo\n\tflex, ok := a.Main.GetPrimitive(\"main\").(*tview.Flex)\n\tif !ok {\n\t\tslog.Error(\"Expecting flex view main panel. Exiting!\")\n\t\tos.Exit(1)\n\t}\n\tif a.showHeader {\n\t\tflex.RemoveItemAtIndex(0)\n\t\tflex.AddItemAtIndex(0, a.buildHeader(), 7, 1, false)\n\t} else {\n\t\tflex.RemoveItemAtIndex(0)\n\t\tflex.AddItemAtIndex(0, a.statusIndicator(), 1, 1, false)\n\t}\n}\n\nfunc (a *App) toggleCrumbs(flag bool) {\n\ta.showCrumbs = flag\n\tflex, ok := a.Main.GetPrimitive(\"main\").(*tview.Flex)\n\tif !ok {\n\t\tslog.Error(\"Expecting valid flex view main panel. Exiting!\")\n\t\tos.Exit(1)\n\t}\n\tif a.showCrumbs {\n\t\tif _, ok := flex.ItemAt(2).(*ui.Crumbs); !ok {\n\t\t\tflex.AddItemAtIndex(2, a.Crumbs(), 1, 1, false)\n\t\t}\n\t} else {\n\t\tflex.RemoveItemAtIndex(2)\n\t}\n}\n\nfunc (a *App) buildHeader() tview.Primitive {\n\theader := tview.NewFlex()\n\theader.SetBackgroundColor(a.Styles.BgColor())\n\theader.SetDirection(tview.FlexColumn)\n\tif !a.showHeader {\n\t\treturn header\n\t}\n\n\tclWidth := clusterInfoWidth\n\tif a.Conn() != nil && a.Conn().ConnectionOK() {\n\t\tn, err := a.Conn().Config().CurrentClusterName()\n\t\tif err == nil {\n\t\t\tsize := len(n) + clusterInfoPad\n\t\t\tif size > clWidth {\n\t\t\t\tclWidth = size\n\t\t\t}\n\t\t}\n\t}\n\theader.AddItem(a.clusterInfo(), clWidth, 1, false)\n\theader.AddItem(a.Menu(), 0, 1, false)\n\n\tif a.showLogo {\n\t\theader.AddItem(a.Logo(), 26, 1, false)\n\t}\n\n\treturn header\n}\n\n// Halt stop the application event loop.\nfunc (a *App) Halt() {\n\tif a.cancelFn != nil {\n\t\ta.cancelFn()\n\t\ta.cancelFn = nil\n\t}\n}\n\n// Resume restarts the app event loop.\nfunc (a *App) Resume() {\n\tvar ctx context.Context\n\tctx, a.cancelFn = context.WithCancel(context.Background())\n\n\tgo a.clusterUpdater(ctx)\n\n\tif a.Config.K9s.UI.Reactive {\n\t\tif err := a.ConfigWatcher(ctx, a); err != nil {\n\t\t\tslog.Warn(\"ConfigWatcher failed\", slogs.Error, err)\n\t\t}\n\t\tif err := a.SkinsDirWatcher(ctx, a); err != nil {\n\t\t\tslog.Warn(\"SkinsWatcher failed\", slogs.Error, err)\n\t\t}\n\t\tif err := a.CustomViewsWatcher(ctx, a); err != nil {\n\t\t\tslog.Warn(\"CustomView watcher failed\", slogs.Error, err)\n\t\t}\n\t}\n}\n\nfunc (a *App) clusterUpdater(ctx context.Context) {\n\tif a.Conn() == nil || !a.Conn().ConnectionOK() || a.factory == nil || a.clusterModel == nil {\n\t\tslog.Debug(\"Skipping cluster updater - no valid connection\")\n\t\treturn\n\t}\n\n\tif err := a.refreshCluster(ctx); err != nil {\n\t\tslog.Error(\"Cluster updater failed!\", slogs.Error, err)\n\t\treturn\n\t}\n\n\tbf := model.NewExpBackOff(ctx, clusterRefresh, 2*time.Minute)\n\tdelay := clusterRefresh\n\tfor {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\tslog.Debug(\"ClusterInfo updater canceled!\")\n\t\t\treturn\n\t\tcase <-time.After(delay):\n\t\t\tif err := a.refreshCluster(ctx); err != nil {\n\t\t\t\tslog.Error(\"Cluster updates failed. Giving up ;(\", slogs.Error, err)\n\t\t\t\tif delay = bf.NextBackOff(); delay == backoff.Stop {\n\t\t\t\t\ta.BailOut(1)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tbf.Reset()\n\t\t\t\tdelay = clusterRefresh\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (a *App) refreshCluster(context.Context) error {\n\tif a.Conn() == nil || a.factory == nil || a.clusterModel == nil {\n\t\treturn nil\n\t}\n\n\tc := a.Content.Top()\n\tif ok := a.Conn().CheckConnectivity(); ok {\n\t\tif atomic.LoadInt32(&a.conRetry) > 0 {\n\t\t\tatomic.StoreInt32(&a.conRetry, 0)\n\t\t\ta.Status(model.FlashInfo, \"K8s connectivity OK\")\n\t\t\tif c != nil {\n\t\t\t\tc.Start()\n\t\t\t}\n\t\t} else {\n\t\t\ta.ClearStatus(true)\n\t\t}\n\t\ta.factory.ValidatePortForwards()\n\t} else if c != nil {\n\t\tatomic.AddInt32(&a.conRetry, 1)\n\t\tc.Stop()\n\t}\n\n\tcount, maxConnRetry := atomic.LoadInt32(&a.conRetry), a.Config.K9s.MaxConnRetry\n\tif count >= maxConnRetry {\n\t\tslog.Error(\"Conn check failed. Bailing out!\",\n\t\t\tslogs.Retry, count,\n\t\t\tslogs.MaxRetries, maxConnRetry,\n\t\t)\n\t\tExitStatus = fmt.Sprintf(\"Lost K8s connection (%d). Bailing out!\", count)\n\t\ta.BailOut(1)\n\t}\n\tif count > 0 {\n\t\ta.Status(model.FlashWarn, fmt.Sprintf(\"Dial K8s Toast [%d/%d]\", count, maxConnRetry))\n\t\treturn fmt.Errorf(\"conn check failed (%d/%d)\", count, maxConnRetry)\n\t}\n\n\t// Reload alias\n\tgo func() {\n\t\tif err := a.command.Reset(a.Config.ContextAliasesPath(), false); err != nil {\n\t\t\tslog.Warn(\"Command reset failed\", slogs.Error, err)\n\t\t\ta.QueueUpdateDraw(func() {\n\t\t\t\ta.Logo().Warn(\"Aliases load failed!\")\n\t\t\t})\n\t\t}\n\t}()\n\t// Update cluster info\n\ta.clusterModel.Refresh()\n\n\treturn nil\n}\n\nfunc (a *App) switchNS(ns string) error {\n\tif a.Config.ActiveNamespace() == ns {\n\t\treturn nil\n\t}\n\tif ns == client.ClusterScope {\n\t\tns = client.BlankNamespace\n\t}\n\tif err := a.Config.SetActiveNamespace(ns); err != nil {\n\t\treturn err\n\t}\n\n\treturn a.factory.SetActiveNS(ns)\n}\n\nfunc (a *App) switchContext(ci *cmd.Interpreter, force bool) error {\n\tcontextName, ok := ci.HasContext()\n\tif (!ok || a.Config.ActiveContextName() == contextName) && !force {\n\t\treturn nil\n\t}\n\ta.Halt()\n\tdefer a.Resume()\n\t{\n\t\ta.Config.Reset()\n\t\tct, err := a.Config.ActivateContext(contextName)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif cns, ok := ci.NSArg(); ok {\n\t\t\tct.Namespace.Active = cns\n\t\t}\n\t\tp := cmd.NewInterpreter(a.Config.ActiveView())\n\t\tp.ResetContextArg()\n\t\tif p.IsContextCmd() {\n\t\t\ta.Config.SetActiveView(client.PodGVR.String())\n\t\t}\n\t\tns := a.Config.ActiveNamespace()\n\t\tif !a.Conn().IsValidNamespace(ns) {\n\t\t\tslog.Warn(\"Unable to validate namespace\", slogs.Namespace, ns)\n\t\t\tif err := a.Config.SetActiveNamespace(ns); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\ta.Flash().Infof(\"Using %q namespace\", ns)\n\n\t\tif err := a.Config.Save(true); err != nil {\n\t\t\tslog.Error(\"Fail to save config to disk\", slogs.Subsys, \"config\", slogs.Error, err)\n\t\t}\n\n\t\tif a.factory == nil && a.Conn() != nil {\n\t\t\ta.factory = watch.NewFactory(a.Conn())\n\t\t\ta.clusterModel = model.NewClusterInfo(a.factory, a.version, a.Config.K9s)\n\t\t\ta.clusterModel.AddListener(a.clusterInfo())\n\t\t\ta.clusterModel.AddListener(a.statusIndicator())\n\t\t}\n\n\t\tif a.factory != nil {\n\t\t\ta.initFactory(ns)\n\t\t}\n\n\t\tif err := a.command.Reset(a.Config.ContextAliasesPath(), true); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tslog.Debug(\"Switching Context\",\n\t\t\tslogs.Context, contextName,\n\t\t\tslogs.Namespace, ns,\n\t\t\tslogs.View, a.Config.ActiveView(),\n\t\t)\n\t\ta.Flash().Infof(\"Switching context to %q::%q\", contextName, ns)\n\t\ta.ReloadStyles()\n\t\ta.gotoResource(a.Config.ActiveView(), \"\", true, true)\n\t\tif a.clusterModel != nil {\n\t\t\tgo a.clusterModel.Reset(a.factory)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (a *App) initFactory(ns string) {\n\ta.factory.Terminate()\n\ta.factory.Start(ns)\n}\n\n// BailOut exists the application.\nfunc (a *App) BailOut(exitCode int) {\n\tdefer func() {\n\t\tif err := recover(); err != nil {\n\t\t\tslog.Error(\"Bailout failed\", slogs.Error, err)\n\t\t}\n\t}()\n\n\tif err := nukeK9sShell(a); err != nil {\n\t\tslog.Error(\"Unable to nuke k9s shell pod\", slogs.Error, err)\n\t}\n\n\ta.stopImgScanner()\n\ta.factory.Terminate()\n\ta.App.BailOut(exitCode)\n}\n\n// Run starts the application loop.\nfunc (a *App) Run() error {\n\ta.Resume()\n\n\tgo func() {\n\t\tif !a.Config.K9s.IsSplashless() {\n\t\t\t<-time.After(splashDelay)\n\t\t}\n\t\ta.QueueUpdateDraw(func() {\n\t\t\ta.Main.SwitchToPage(\"main\")\n\t\t\t// if command bar is already active, focus it\n\t\t\tif a.CmdBuff().IsActive() {\n\t\t\t\ta.SetFocus(a.Prompt())\n\t\t\t}\n\t\t})\n\t}()\n\n\tif err := a.command.defaultCmd(true); err != nil {\n\t\treturn err\n\t}\n\ta.SetRunning(true)\n\tif err := a.Application.Run(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Status reports a new app status for display.\nfunc (a *App) Status(l model.FlashLevel, msg string) {\n\ta.QueueUpdateDraw(func() {\n\t\tif a.showHeader {\n\t\t\ta.setLogo(l, msg)\n\t\t} else {\n\t\t\ta.setIndicator(l, msg)\n\t\t}\n\t})\n}\n\n// IsBenchmarking check if benchmarks are active.\nfunc (a *App) IsBenchmarking() bool {\n\treturn a.Logo().IsBenchmarking()\n}\n\n// ClearStatus reset logo back to normal.\nfunc (a *App) ClearStatus(flash bool) {\n\ta.QueueUpdate(func() {\n\t\ta.Logo().Reset()\n\t\tif flash {\n\t\t\ta.Flash().Clear()\n\t\t}\n\t})\n}\n\nfunc (a *App) setLogo(l model.FlashLevel, msg string) {\n\tswitch l {\n\tcase model.FlashErr:\n\t\ta.Logo().Err(msg)\n\tcase model.FlashWarn:\n\t\ta.Logo().Warn(msg)\n\tcase model.FlashInfo:\n\t\ta.Logo().Info(msg)\n\tdefault:\n\t\ta.Logo().Reset()\n\t}\n}\n\nfunc (a *App) setIndicator(l model.FlashLevel, msg string) {\n\tswitch l {\n\tcase model.FlashErr:\n\t\ta.statusIndicator().Err(msg)\n\tcase model.FlashWarn:\n\t\ta.statusIndicator().Warn(msg)\n\tcase model.FlashInfo:\n\t\ta.statusIndicator().Info(msg)\n\tdefault:\n\t\ta.statusIndicator().Reset()\n\t}\n}\n\n// PrevCmd pops the command stack.\nfunc (a *App) PrevCmd(*tcell.EventKey) *tcell.EventKey {\n\tif !a.Content.IsLast() {\n\t\ta.Content.Pop()\n\t}\n\n\treturn nil\n}\n\nfunc (a *App) toggleHeaderCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif a.Prompt().InCmdMode() {\n\t\treturn evt\n\t}\n\n\ta.QueueUpdateDraw(func() {\n\t\ta.showHeader = !a.showHeader\n\t\ta.toggleHeader(a.showHeader, a.showLogo)\n\t})\n\n\treturn nil\n}\n\nfunc (a *App) toggleCrumbsCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif a.Prompt().InCmdMode() {\n\t\treturn evt\n\t}\n\n\ta.QueueUpdateDraw(func() {\n\t\ta.showCrumbs = !a.showCrumbs\n\t\ta.toggleCrumbs(a.showCrumbs)\n\t})\n\n\treturn nil\n}\n\nfunc (a *App) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif a.CmdBuff().IsActive() && !a.CmdBuff().Empty() {\n\t\ta.gotoResource(a.GetCmd(), \"\", true, true)\n\t\ta.ResetCmd()\n\t\treturn nil\n\t}\n\n\treturn evt\n}\n\nfunc (a *App) cowCmd(msg string) {\n\td := a.Styles.Dialog()\n\tdialog.ShowError(&d, a.Content.Pages, msg)\n}\n\nfunc (a *App) dirCmd(path string, pushCmd bool) error {\n\tslog.Debug(\"Exec Dir command\", slogs.Path, path)\n\t_, err := os.Stat(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif path == \".\" {\n\t\tdir, err := os.Getwd()\n\t\tif err == nil {\n\t\t\tpath = dir\n\t\t}\n\t}\n\tif pushCmd {\n\t\ta.cmdHistory.Push(\"dir \" + path)\n\t}\n\n\treturn a.inject(NewDir(path), true)\n}\n\nfunc (a *App) quitCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tnoExit := a.Config.K9s.NoExitOnCtrlC\n\tif a.InCmdMode() {\n\t\tif isBailoutEvt(evt) && noExit {\n\t\t\treturn nil\n\t\t}\n\t\treturn evt\n\t}\n\n\tif !noExit {\n\t\ta.BailOut(0)\n\t}\n\n\treturn nil\n}\n\nfunc (a *App) helpCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif evt != nil && evt.Rune() == '?' && a.Prompt().InCmdMode() {\n\t\treturn evt\n\t}\n\n\ttop := a.Content.Top()\n\tif top != nil && top.Name() == \"help\" {\n\t\ta.Content.Pop()\n\t\treturn nil\n\t}\n\n\tif err := a.inject(NewHelp(a), false); err != nil {\n\t\ta.Flash().Err(err)\n\t}\n\n\ta.Prompt().Deactivate()\n\treturn nil\n}\n\n// previousCommand returns to the command prior to the current one in the history\nfunc (a *App) previousCommand(evt *tcell.EventKey) *tcell.EventKey {\n\tif evt != nil && evt.Rune() == rune(ui.KeyLeftBracket) && a.Prompt().InCmdMode() {\n\t\treturn evt\n\t}\n\tc, ok := a.cmdHistory.Back()\n\tif !ok {\n\t\ta.App.Flash().Warn(\"Can't go back any further\")\n\t\treturn evt\n\t}\n\ta.gotoResource(c, \"\", true, false)\n\treturn nil\n}\n\n// nextCommand returns to the command subsequent to the current one in the history\nfunc (a *App) nextCommand(evt *tcell.EventKey) *tcell.EventKey {\n\tif evt != nil && evt.Rune() == rune(ui.KeyRightBracket) && a.Prompt().InCmdMode() {\n\t\treturn evt\n\t}\n\tc, ok := a.cmdHistory.Forward()\n\tif !ok {\n\t\ta.App.Flash().Warn(\"Can't go forward any further\")\n\t\treturn evt\n\t}\n\t// We go to the resource before updating the history so that\n\t// gotoResource doesn't add this command to the history\n\ta.gotoResource(c, \"\", true, false)\n\treturn nil\n}\n\n// lastCommand switches between the last command and the current one a la `cd -`\nfunc (a *App) lastCommand(evt *tcell.EventKey) *tcell.EventKey {\n\tif evt != nil && evt.Rune() == ui.KeyDash && a.Prompt().InCmdMode() {\n\t\treturn evt\n\t}\n\tc, ok := a.cmdHistory.Top()\n\tif !ok {\n\t\ta.App.Flash().Warn(\"No previous view to switch to\")\n\t\treturn evt\n\t}\n\ta.gotoResource(c, \"\", true, false)\n\n\treturn nil\n}\n\nfunc (a *App) aliasCmd(*tcell.EventKey) *tcell.EventKey {\n\tif a.Content.Top() != nil && a.Content.Top().Name() == aliasTitle {\n\t\ta.Content.Pop()\n\t\treturn nil\n\t}\n\n\tif err := a.inject(NewAlias(client.AliGVR), false); err != nil {\n\t\ta.Flash().Err(err)\n\t}\n\n\treturn nil\n}\n\nfunc (a *App) gotoResource(c, path string, clearStack, pushCmd bool) {\n\terr := a.command.run(cmd.NewInterpreter(c), path, clearStack, pushCmd)\n\tif err != nil {\n\t\td := a.Styles.Dialog()\n\t\tdialog.ShowError(&d, a.Content.Pages, err.Error())\n\t}\n}\n\nfunc (a *App) inject(c model.Component, clearStack bool) error {\n\tctx := context.WithValue(context.Background(), internal.KeyApp, a)\n\tif err := c.Init(ctx); err != nil {\n\t\tslog.Error(\"Component init failed\",\n\t\t\tslogs.Error, err,\n\t\t\tslogs.CompName, c.Name(),\n\t\t)\n\t\treturn err\n\t}\n\tif clearStack {\n\t\ta.Content.Clear()\n\t}\n\ta.Content.Push(c)\n\n\treturn nil\n}\n\nfunc (a *App) clusterInfo() *ClusterInfo {\n\treturn a.Views()[\"clusterInfo\"].(*ClusterInfo)\n}\n\nfunc (a *App) statusIndicator() *ui.StatusIndicator {\n\treturn a.Views()[\"statusIndicator\"].(*ui.StatusIndicator)\n}\n"
  },
  {
    "path": "internal/view/app_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config/mock\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestAppNew(t *testing.T) {\n\ta := view.NewApp(mock.NewMockConfig(t))\n\t_ = a.Init(\"blee\", 10)\n\n\tassert.Equal(t, 14, a.GetActions().Len())\n}\n"
  },
  {
    "path": "internal/view/benchmark.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\n// Benchmark represents a service benchmark results view.\ntype Benchmark struct {\n\tResourceViewer\n}\n\n// NewBenchmark returns a new viewer.\nfunc NewBenchmark(gvr *client.GVR) ResourceViewer {\n\tb := Benchmark{\n\t\tResourceViewer: NewBrowser(gvr),\n\t}\n\tb.GetTable().SetBorderFocusColor(tcell.ColorSeaGreen)\n\tb.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorSeaGreen).Attributes(tcell.AttrNone))\n\tb.GetTable().SetSortCol(ageCol, true)\n\tb.SetContextFn(b.benchContext)\n\tb.GetTable().SetEnterFn(b.viewBench)\n\n\treturn &b\n}\n\nfunc (b *Benchmark) benchContext(ctx context.Context) context.Context {\n\treturn context.WithValue(ctx, internal.KeyDir, benchDir(b.App().Config))\n}\n\nfunc (b *Benchmark) viewBench(app *App, _ ui.Tabular, _ *client.GVR, path string) {\n\tmdata, err := readBenchFile(app.Config, b.benchFile())\n\tif err != nil {\n\t\tapp.Flash().Errf(\"Unable to load bench file %s\", err)\n\t\treturn\n\t}\n\n\tdetails := NewDetails(b.App(), \"Results\", fileToSubject(path), contentYAML, false).Update(mdata)\n\tif err := app.inject(details, false); err != nil {\n\t\tapp.Flash().Err(err)\n\t}\n}\n\nfunc (b *Benchmark) benchFile() string {\n\tr := b.GetTable().GetSelectedRowIndex()\n\treturn ui.TrimCell(b.GetTable().SelectTable, r, 7)\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc fileToSubject(path string) string {\n\ttokens := strings.Split(path, \"/\")\n\tee := strings.Split(tokens[len(tokens)-1], \"_\")\n\treturn ee[0] + \"/\" + ee[1]\n}\n\nfunc benchDir(cfg *config.Config) string {\n\tct, err := cfg.K9s.ActiveContext()\n\tif err != nil {\n\t\tslog.Error(\"No active context located\", slogs.Error, err)\n\t\treturn render.MissingValue\n\t}\n\n\treturn filepath.Join(\n\t\tconfig.AppBenchmarksDir,\n\t\tdata.SanitizeFileName(ct.ClusterName),\n\t\tdata.SanitizeFileName(cfg.K9s.ActiveContextName()),\n\t)\n}\n\nfunc readBenchFile(cfg *config.Config, n string) (string, error) {\n\tbb, err := os.ReadFile(filepath.Join(benchDir(cfg), n))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(bb), nil\n}\n"
  },
  {
    "path": "internal/view/browser.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/ui/dialog\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n\t\"github.com/derailed/tcell/v2\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/util/sets\"\n)\n\n// Browser represents a generic resource browser.\ntype Browser struct {\n\t*Table\n\n\tnamespaces map[int]string\n\tmeta       *metav1.APIResource\n\taccessor   dao.Accessor\n\tcontextFn  ContextFunc\n\tcancelFn   context.CancelFunc\n\tmx         sync.RWMutex\n\tupdating   bool\n\tfirstView  atomic.Int32\n}\n\n// NewBrowser returns a new browser.\nfunc NewBrowser(gvr *client.GVR) ResourceViewer {\n\treturn &Browser{\n\t\tTable: NewTable(gvr),\n\t}\n}\n\nfunc (b *Browser) setUpdating(f bool) {\n\tb.mx.Lock()\n\tdefer b.mx.Unlock()\n\tb.updating = f\n}\n\nfunc (b *Browser) getUpdating() bool {\n\tb.mx.RLock()\n\tdefer b.mx.RUnlock()\n\treturn b.updating\n}\n\n// SetCommand sets the current command.\nfunc (b *Browser) SetCommand(i *cmd.Interpreter) {\n\tb.GetTable().SetCommand(i)\n}\n\n// Init watches all running pods in given namespace.\nfunc (b *Browser) Init(ctx context.Context) error {\n\tvar err error\n\n\tb.meta, err = dao.MetaAccess.MetaFor(b.GVR())\n\tif err != nil {\n\t\treturn err\n\t}\n\tcolorerFn := model1.DefaultColorer\n\tif r, ok := model.Registry[b.GVR()]; ok && r.Renderer != nil {\n\t\tcolorerFn = r.Renderer.ColorerFunc()\n\t}\n\tb.GetTable().SetColorerFn(colorerFn)\n\n\tif e := b.Table.Init(ctx); e != nil {\n\t\treturn e\n\t}\n\tns := client.CleanseNamespace(b.app.Config.ActiveNamespace())\n\tif dao.IsK8sMeta(b.meta) && b.app.ConOK() {\n\t\tif _, e := b.app.factory.CanForResource(ns, b.GVR(), client.ListAccess); e != nil {\n\t\t\treturn e\n\t\t}\n\t}\n\tif b.App().IsRunning() {\n\t\tb.app.CmdBuff().Reset()\n\t}\n\tb.SetReadOnly(b.app.Config.IsReadOnly())\n\tb.SetNoIcon(b.app.Config.K9s.UI.NoIcons)\n\tb.SetFullGVR(b.app.Config.K9s.UI.UseFullGVRTitle)\n\n\tb.bindKeys(b.Actions())\n\tfor _, f := range b.bindKeysFn {\n\t\tf(b.Actions())\n\t}\n\tb.accessor, err = dao.AccessorFor(b.app.factory, b.GVR())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tb.setNamespace(ns)\n\trow, _ := b.GetSelection()\n\tif row == 0 && b.GetRowCount() > 0 {\n\t\tb.Select(1, 0)\n\t}\n\tb.GetModel().SetRefreshRate(b.App().Config.K9s.RefreshDuration())\n\n\tb.CmdBuff().SetSuggestionFn(b.suggestFilter())\n\n\treturn nil\n}\n\n// InCmdMode checks if prompt is active.\nfunc (b *Browser) InCmdMode() bool {\n\treturn b.CmdBuff().InCmdMode()\n}\n\nfunc (b *Browser) suggestFilter() model.SuggestionFunc {\n\treturn func(s string) (entries sort.StringSlice) {\n\t\tif s == \"\" {\n\t\t\tif b.App().filterHistory.Empty() {\n\t\t\t\treturn\n\t\t\t}\n\t\t\treturn b.App().filterHistory.List()\n\t\t}\n\n\t\ts = strings.ToLower(s)\n\t\tfor _, h := range b.App().filterHistory.List() {\n\t\t\tif s == h {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif strings.HasPrefix(h, s) {\n\t\t\t\tentries = append(entries, strings.Replace(h, s, \"\", 1))\n\t\t\t}\n\t\t}\n\t\treturn\n\t}\n}\n\nfunc (b *Browser) bindKeys(aa *ui.KeyActions) {\n\taa.Bulk(ui.KeyMap{\n\t\ttcell.KeyEscape: ui.NewSharedKeyAction(\"Filter Reset\", b.resetCmd, false),\n\t\tui.KeyQ:         ui.NewSharedKeyAction(\"Filter Reset\", b.resetCmd, false),\n\t\ttcell.KeyEnter:  ui.NewSharedKeyAction(\"Filter\", b.filterCmd, false),\n\t\ttcell.KeyHelp:   ui.NewSharedKeyAction(\"Help\", b.helpCmd, false),\n\t})\n}\n\n// SetInstance sets a single instance view.\nfunc (b *Browser) SetInstance(path string) {\n\tb.GetModel().SetInstance(path)\n}\n\n// Start initializes browser updates.\nfunc (b *Browser) Start() {\n\tns := b.app.Config.ActiveNamespace()\n\tif n := b.GetModel().GetNamespace(); !client.IsClusterScoped(n) {\n\t\tns = n\n\t}\n\tif err := b.app.switchNS(ns); err != nil {\n\t\tslog.Error(\"Unable to switch namespace\", slogs.Error, err)\n\t}\n\n\tb.Stop()\n\tb.firstView.Store(0) // Reset first view counter on each start\n\tb.GetModel().AddListener(b)\n\tb.Table.Start()\n\tb.CmdBuff().AddListener(b)\n\tif err := b.GetModel().Watch(b.prepareContext()); err != nil {\n\t\tgo func() {\n\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\tb.app.QueueUpdateDraw(func() {\n\t\t\t\tb.App().Flash().Errf(\"Watcher failed for %s -- %s\", b.GVR(), err)\n\t\t\t})\n\t\t}()\n\t}\n}\n\n// Stop terminates browser updates.\nfunc (b *Browser) Stop() {\n\tb.mx.Lock()\n\tif b.cancelFn != nil {\n\t\tb.cancelFn()\n\t\tb.cancelFn = nil\n\t}\n\tb.mx.Unlock()\n\tb.GetModel().RemoveListener(b)\n\tb.CmdBuff().RemoveListener(b)\n\tb.Table.Stop()\n}\n\nfunc (b *Browser) SetFilter(s string, wipe bool) {\n\tb.CmdBuff().SetText(s, \"\", wipe)\n}\n\nfunc (b *Browser) SetLabelSelector(sel labels.Selector, wipe bool) {\n\tif sel != nil {\n\t\tb.CmdBuff().SetText(sel.String(), \"\", wipe)\n\t}\n\tb.GetModel().SetLabelSelector(sel)\n}\n\n// BufferChanged indicates the buffer was changed.\nfunc (*Browser) BufferChanged(_, _ string) {}\n\n// BufferCompleted indicates input was accepted.\nfunc (b *Browser) BufferCompleted(text, _ string) {\n\tif internal.IsLabelSelector(text) {\n\t\tif sel, err := ui.ExtractLabelSelector(text); err == nil {\n\t\t\tb.GetModel().SetLabelSelector(sel)\n\t\t}\n\t} else {\n\t\tb.GetModel().SetLabelSelector(labels.Everything())\n\t}\n}\n\n// BufferActive indicates the buff activity changed.\nfunc (b *Browser) BufferActive(state bool, _ model.BufferKind) {\n\tif state {\n\t\treturn\n\t}\n\tif err := b.GetModel().Refresh(b.GetContext()); err != nil {\n\t\tslog.Error(\"Model refresh failed\",\n\t\t\tslogs.GVR, b.GVR(),\n\t\t\tslogs.Error, err,\n\t\t)\n\t}\n\tmdata := b.GetModel().Peek()\n\tcdata := b.Update(mdata, b.App().Conn().HasMetrics())\n\tb.app.QueueUpdateDraw(func() {\n\t\tif b.getUpdating() {\n\t\t\treturn\n\t\t}\n\t\tb.setUpdating(true)\n\t\tdefer b.setUpdating(false)\n\t\tb.UpdateUI(cdata, mdata)\n\t\tif b.GetRowCount() > 1 {\n\t\t\tb.App().filterHistory.Push(b.CmdBuff().GetText())\n\t\t}\n\t})\n}\n\nfunc (b *Browser) prepareContext() context.Context {\n\tctx := b.defaultContext()\n\n\tb.mx.Lock()\n\tif b.cancelFn != nil {\n\t\tb.cancelFn()\n\t}\n\tctx, b.cancelFn = context.WithCancel(ctx)\n\tb.mx.Unlock()\n\n\tif b.contextFn != nil {\n\t\tctx = b.contextFn(ctx)\n\t}\n\tif path, ok := ctx.Value(internal.KeyPath).(string); ok && path != \"\" {\n\t\tb.Path = path\n\t}\n\tb.mx.Lock()\n\tb.SetContext(ctx)\n\tb.mx.Unlock()\n\n\treturn ctx\n}\n\nfunc (b *Browser) refresh() {\n\tb.Start()\n}\n\n// Name returns the component name.\nfunc (b *Browser) Name() string { return b.meta.Kind }\n\n// SetContextFn populates a custom context.\nfunc (b *Browser) SetContextFn(f ContextFunc) { b.contextFn = f }\n\n// GetTable returns the underlying table.\nfunc (b *Browser) GetTable() *Table { return b.Table }\n\n// Aliases returns all available aliases.\nfunc (b *Browser) Aliases() sets.Set[string] {\n\treturn aliases(b.meta, b.app.command.AliasesFor(client.NewGVRFromMeta(b.meta)))\n}\n\n// ----------------------------------------------------------------------------\n// Model Protocol...\n\n// TableNoData notifies view no data is available.\nfunc (b *Browser) TableNoData(mdata *model1.TableData) {\n\tvar cancel context.CancelFunc\n\tb.mx.RLock()\n\tcancel = b.cancelFn\n\tb.mx.RUnlock()\n\n\tif !b.app.ConOK() || cancel == nil || !b.app.IsRunning() {\n\t\treturn\n\t}\n\t// Skip warning on first view (likely during initialization)\n\tif b.firstView.Load() == 0 || mdata.HeaderCount() == 0 {\n\t\tb.firstView.Add(1)\n\t\treturn\n\t}\n\n\tcdata := b.Update(mdata, b.app.Conn().HasMetrics())\n\tb.app.QueueUpdateDraw(func() {\n\t\tif b.getUpdating() {\n\t\t\treturn\n\t\t}\n\t\tb.setUpdating(true)\n\t\tdefer b.setUpdating(false)\n\t\tif b.GetColumnCount() == 0 {\n\t\t\tb.app.Flash().Warnf(\"No resources found for %s in %q namespace\", b.GVR(), client.PrintNamespace(b.GetNamespace()))\n\t\t}\n\t\tb.refreshActions()\n\t\tb.UpdateUI(cdata, mdata)\n\t})\n}\n\n// TableDataChanged notifies view new data is available.\nfunc (b *Browser) TableDataChanged(mdata *model1.TableData) {\n\tvar cancel context.CancelFunc\n\tb.mx.RLock()\n\tcancel = b.cancelFn\n\tb.mx.RUnlock()\n\n\tif cancel == nil || !b.app.IsRunning() {\n\t\treturn\n\t}\n\n\tcdata := b.Update(mdata, b.app.Conn().HasMetrics())\n\tb.app.QueueUpdateDraw(func() {\n\t\tif b.getUpdating() {\n\t\t\treturn\n\t\t}\n\t\tb.setUpdating(true)\n\t\tdefer b.setUpdating(false)\n\t\tif b.GetColumnCount() == 0 {\n\t\t\tif client.IsClusterScoped(b.GetNamespace()) {\n\t\t\t\tb.app.Flash().Infof(\"Viewing %s...\", b.GVR())\n\t\t\t} else {\n\t\t\t\tb.app.Flash().Infof(\"Viewing %s in namespace %s\", b.GVR(), client.PrintNamespace(b.GetNamespace()))\n\t\t\t}\n\t\t}\n\t\tb.refreshActions()\n\t\tb.UpdateUI(cdata, mdata)\n\t})\n}\n\n// TableLoadFailed notifies view something went south.\nfunc (b *Browser) TableLoadFailed(err error) {\n\tb.app.QueueUpdateDraw(func() {\n\t\tb.app.Flash().Err(err)\n\t\tb.App().ClearStatus(false)\n\t})\n}\n\n// ----------------------------------------------------------------------------\n// Actions...\n\nfunc (b *Browser) nsWarpCmd(*tcell.EventKey) *tcell.EventKey {\n\tpath := b.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn nil\n\t}\n\n\to, err := b.app.factory.Get(b.GVR(), path, true, nil)\n\tif err != nil {\n\t\treturn nil\n\t}\n\tu, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn nil\n\t}\n\tb.App().gotoResource(b.GVR().String()+\" \"+u.GetNamespace(), \"\", true, true)\n\n\treturn nil\n}\n\nfunc (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := b.GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\n\tv := NewLiveView(b.app, yamlAction, model.NewYAML(b.GVR(), path))\n\tif err := v.app.inject(v, false); err != nil {\n\t\tv.app.Flash().Err(err)\n\t}\n\n\treturn nil\n}\n\nfunc (b *Browser) helpCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif b.CmdBuff().InCmdMode() {\n\t\treturn nil\n\t}\n\n\treturn evt\n}\n\nfunc (b *Browser) resetCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif !b.CmdBuff().InCmdMode() {\n\t\thasFilter := !b.CmdBuff().Empty()\n\t\tb.CmdBuff().ClearText(false)\n\t\tif hasFilter {\n\t\t\tb.GetModel().SetLabelSelector(labels.Everything())\n\t\t\tb.Refresh()\n\t\t}\n\t\treturn b.App().PrevCmd(evt)\n\t}\n\n\tb.CmdBuff().Reset()\n\tif internal.IsLabelSelector(b.CmdBuff().GetText()) {\n\t\tb.Start()\n\t}\n\tb.Refresh()\n\n\treturn nil\n}\n\nfunc (b *Browser) filterCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif !b.CmdBuff().IsActive() {\n\t\treturn evt\n\t}\n\n\tb.CmdBuff().SetActive(false)\n\tif internal.IsLabelSelector(b.CmdBuff().GetText()) {\n\t\tb.Start()\n\t\treturn nil\n\t}\n\tb.Refresh()\n\n\treturn nil\n}\n\nfunc (b *Browser) enterCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := b.GetSelectedItem()\n\tif b.filterCmd(evt) == nil || path == \"\" {\n\t\treturn nil\n\t}\n\n\tf := describeResource\n\tif b.enterFn != nil {\n\t\tf = b.enterFn\n\t}\n\tf(b.app, b.GetModel(), b.GVR(), path)\n\n\treturn nil\n}\n\nfunc (b *Browser) refreshCmd(*tcell.EventKey) *tcell.EventKey {\n\tb.app.Flash().Info(\"Refreshing...\")\n\tb.refresh()\n\n\treturn nil\n}\n\nfunc (b *Browser) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tselections := b.GetSelectedItems()\n\tif len(selections) == 0 {\n\t\treturn evt\n\t}\n\n\tb.Stop()\n\tdefer b.Start()\n\t{\n\t\tmsg := fmt.Sprintf(\"Delete %s %s?\", b.GVR().R(), selections[0])\n\t\tif len(selections) > 1 {\n\t\t\tmsg = fmt.Sprintf(\"Delete %d marked %s?\", len(selections), b.GVR())\n\t\t}\n\t\tif !dao.IsK8sMeta(b.meta) {\n\t\t\tb.simpleDelete(selections, msg)\n\t\t\treturn nil\n\t\t}\n\t\tb.resourceDelete(selections, msg)\n\t}\n\n\treturn nil\n}\n\nfunc (b *Browser) describeCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := b.GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\tdescribeResource(b.app, b.GetModel(), b.GVR(), path)\n\n\treturn nil\n}\n\nfunc (b *Browser) editCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := b.GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\n\tb.Stop()\n\tdefer b.Start()\n\tif err := editRes(b.app, b.GVR(), path); err != nil {\n\t\tb.App().Flash().Err(err)\n\t}\n\n\treturn nil\n}\n\nfunc editRes(app *App, gvr *client.GVR, path string) error {\n\tif path == \"\" {\n\t\treturn fmt.Errorf(\"nothing selected %q\", path)\n\t}\n\tns, n := client.Namespaced(path)\n\tif n == \"\" {\n\t\treturn fmt.Errorf(\"missing resource name in path %q\", path)\n\t}\n\tif client.IsClusterScoped(ns) {\n\t\tns = client.BlankNamespace\n\t}\n\tif ok, err := app.Conn().CanI(ns, gvr, n, client.PatchAccess); !ok || err != nil {\n\t\treturn fmt.Errorf(\"current user can't edit resource %s\", gvr)\n\t}\n\n\targs := make([]string, 0, 10)\n\targs = append(args, \"edit\", gvr.FQN(n))\n\tif ns != client.BlankNamespace {\n\t\targs = append(args, \"-n\", ns)\n\t}\n\tif err := runK(app, &shellOpts{clear: true, args: args}); err != nil {\n\t\tapp.Flash().Errf(\"Edit command failed: %s\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey {\n\ti, err := strconv.Atoi(string(evt.Rune()))\n\tif err != nil {\n\t\tslog.Error(\"Unable to convert keystroke\", slogs.Error, err)\n\t\treturn nil\n\t}\n\tns := b.namespaces[i]\n\n\tauth, err := b.App().factory.Client().CanI(ns, b.GVR(), \"\", client.ListAccess)\n\tif !auth {\n\t\tif err == nil {\n\t\t\terr = fmt.Errorf(\"access denied for user on: %s/%s\", ns, b.GVR())\n\t\t}\n\t\tb.App().Flash().Err(err)\n\t\treturn nil\n\t}\n\n\tif err := b.app.switchNS(ns); err != nil {\n\t\tb.App().Flash().Err(err)\n\t\treturn nil\n\t}\n\tb.setNamespace(ns)\n\tif client.IsClusterScoped(ns) {\n\t\tb.app.Flash().Infof(\"Viewing %s...\", b.GVR())\n\t} else {\n\t\tb.app.Flash().Infof(\"Viewing %s in namespace `%s`...\", b.GVR(), client.PrintNamespace(ns))\n\t}\n\tb.refresh()\n\tb.UpdateTitle()\n\tb.SelectRow(1, 0, true)\n\tb.app.CmdBuff().Reset()\n\tif err := b.app.Config.SetActiveNamespace(b.GetModel().GetNamespace()); err != nil {\n\t\tslog.Error(\"Unable to set active namespace during ns switch\", slogs.Error, err)\n\t}\n\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc (b *Browser) setNamespace(ns string) {\n\tns = client.CleanseNamespace(ns)\n\tif b.GetModel().InNamespace(ns) {\n\t\treturn\n\t}\n\tif !b.meta.Namespaced {\n\t\tns = client.ClusterScope\n\t}\n\tb.GetModel().SetNamespace(ns)\n}\n\nfunc (b *Browser) defaultContext() context.Context {\n\tctx := context.WithValue(context.Background(), internal.KeyFactory, b.app.factory)\n\tctx = context.WithValue(ctx, internal.KeyGVR, b.GVR())\n\tctx = context.WithValue(ctx, internal.KeyPath, b.Path)\n\tif internal.IsLabelSelector(b.CmdBuff().GetText()) {\n\t\tif sel, err := ui.ExtractLabelSelector(b.CmdBuff().GetText()); err == nil {\n\t\t\tctx = context.WithValue(ctx, internal.KeyLabels, sel)\n\t\t}\n\t}\n\tctx = context.WithValue(ctx, internal.KeyNamespace, client.CleanseNamespace(b.App().Config.ActiveNamespace()))\n\tctx = context.WithValue(ctx, internal.KeyWithMetrics, b.app.factory.Client().HasMetrics())\n\n\treturn ctx\n}\n\nfunc (b *Browser) refreshActions() {\n\tif top := b.App().Content.Top(); top != nil && top.Name() != b.Name() {\n\t\treturn\n\t}\n\taa := ui.NewKeyActionsFromMap(ui.KeyMap{\n\t\tui.KeyC:        ui.NewKeyAction(\"Copy\", b.cpCmd, false),\n\t\ttcell.KeyEnter: ui.NewKeyAction(\"View\", b.enterCmd, false),\n\t\ttcell.KeyCtrlR: ui.NewKeyAction(\"Refresh\", b.refreshCmd, false),\n\t})\n\n\tif b.app.ConOK() {\n\t\tb.namespaceActions(aa)\n\t\tif !b.app.Config.IsReadOnly() {\n\t\t\tif client.Can(b.meta.Verbs, \"edit\") {\n\t\t\t\taa.Add(ui.KeyE, ui.NewKeyActionWithOpts(\"Edit\", b.editCmd,\n\t\t\t\t\tui.ActionOpts{\n\t\t\t\t\t\tVisible:   true,\n\t\t\t\t\t\tDangerous: true,\n\t\t\t\t\t}))\n\t\t\t}\n\t\t\tif client.Can(b.meta.Verbs, \"delete\") {\n\t\t\t\taa.Add(tcell.KeyCtrlD, ui.NewKeyActionWithOpts(\"Delete\", b.deleteCmd,\n\t\t\t\t\tui.ActionOpts{\n\t\t\t\t\t\tVisible:   true,\n\t\t\t\t\t\tDangerous: true,\n\t\t\t\t\t}))\n\t\t\t}\n\t\t} else {\n\t\t\tb.Actions().ClearDanger()\n\t\t}\n\t}\n\tif !dao.IsK9sMeta(b.meta) {\n\t\taa.Add(ui.KeyY, ui.NewKeyAction(yamlAction, b.viewCmd, true))\n\t\taa.Add(ui.KeyD, ui.NewKeyAction(\"Describe\", b.describeCmd, true))\n\t}\n\tfor _, f := range b.bindKeysFn {\n\t\tf(aa)\n\t}\n\tb.Actions().Merge(aa)\n\n\tif err := pluginActions(b, b.Actions()); err != nil {\n\t\tslog.Warn(\"Plugins load failed\", slogs.Error, err)\n\t\tb.app.Logo().Warn(\"Plugins load failed!\")\n\t}\n\tif err := hotKeyActions(b, b.Actions()); err != nil {\n\t\tslog.Warn(\"Hotkeys load failed\", slogs.Error, err)\n\t\tb.app.Logo().Warn(\"HotKeys load failed!\")\n\t}\n\tb.app.Menu().HydrateMenu(b.Hints())\n}\n\nfunc (b *Browser) namespaceActions(aa *ui.KeyActions) {\n\tif !b.meta.Namespaced || b.GetTable().Path != \"\" {\n\t\treturn\n\t}\n\taa.Add(ui.KeyN, ui.NewKeyAction(\"Copy Namespace\", b.cpNsCmd, false))\n\tif b.meta.Namespaced {\n\t\taa.Add(ui.KeyW, ui.NewKeyAction(\"Warp To Namespace\", b.nsWarpCmd, true))\n\t}\n\n\tb.namespaces = make(map[int]string, data.MaxFavoritesNS)\n\tvar index int\n\tif ok, _ := b.app.Conn().CanI(client.NamespaceAll, client.NsGVR, \"\", client.ListAccess); ok {\n\t\taa.Add(ui.Key0, ui.NewKeyAction(client.NamespaceAll, b.switchNamespaceCmd, true))\n\t\tb.namespaces[0] = client.NamespaceAll\n\t\tindex = 1\n\t}\n\tfavNamespaces := b.app.Config.FavNamespaces()\n\tfor _, ns := range favNamespaces {\n\t\tif ns == client.NamespaceAll {\n\t\t\tcontinue\n\t\t}\n\t\tif numKey, ok := ui.NumKeys[index]; ok {\n\t\t\taa.Add(numKey, ui.NewKeyAction(ns, b.switchNamespaceCmd, true))\n\t\t\tb.namespaces[index] = ns\n\t\t\tindex++\n\t\t} else {\n\t\t\tslog.Warn(\"No number key available for favorite namespace. Skipping...\",\n\t\t\t\tslogs.Namespace, ns,\n\t\t\t\tslogs.Index, index,\n\t\t\t\tslogs.Max, len(favNamespaces),\n\t\t\t)\n\t\t\tbreak\n\t\t}\n\t}\n}\n\nfunc (b *Browser) simpleDelete(selections []string, msg string) {\n\td := b.app.Styles.Dialog()\n\tdialog.ShowConfirm(&d, b.app.Content.Pages, \"Confirm Delete\", msg, func() {\n\t\tb.ShowDeleted()\n\t\tif len(selections) > 1 {\n\t\t\tb.app.Flash().Infof(\"Delete %d marked %s\", len(selections), b.GVR().R())\n\t\t} else {\n\t\t\tb.app.Flash().Infof(\"Delete resource %s %s\", b.GVR(), selections[0])\n\t\t}\n\t\tfor _, sel := range selections {\n\t\t\tnuker, ok := b.accessor.(dao.Nuker)\n\t\t\tif !ok {\n\t\t\t\tb.app.Flash().Errf(\"Invalid nuker %T\", b.accessor)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err := nuker.Delete(context.Background(), sel, nil, dao.DefaultGrace); err != nil {\n\t\t\t\tb.app.Flash().Errf(\"Delete failed with `%s\", err)\n\t\t\t} else {\n\t\t\t\tb.app.factory.DeleteForwarder(sel)\n\t\t\t}\n\t\t\tb.GetTable().DeleteMark(sel)\n\t\t}\n\t\tb.refresh()\n\t}, func() {})\n}\n\nfunc (b *Browser) resourceDelete(selections []string, msg string) {\n\tokFn := func(propagation *metav1.DeletionPropagation, force bool) {\n\t\tb.ShowDeleted()\n\t\tif len(selections) > 1 {\n\t\t\tb.app.Flash().Infof(\"Delete %d marked %s\", len(selections), b.GVR())\n\t\t} else {\n\t\t\tb.app.Flash().Infof(\"Delete resource %s %s\", b.GVR(), selections[0])\n\t\t}\n\t\tfor _, sel := range selections {\n\t\t\tgrace := dao.DefaultGrace\n\t\t\tif force {\n\t\t\t\tgrace = dao.ForceGrace\n\t\t\t}\n\t\t\tif err := b.GetModel().Delete(b.defaultContext(), sel, propagation, grace); err != nil {\n\t\t\t\tb.app.Flash().Errf(\"Delete failed with `%s\", err)\n\t\t\t} else {\n\t\t\t\tb.app.factory.DeleteForwarder(sel)\n\t\t\t}\n\t\t\tb.GetTable().DeleteMark(sel)\n\t\t}\n\t\tb.refresh()\n\t}\n\td := b.app.Styles.Dialog()\n\tdialog.ShowDelete(&d, b.app.Content.Pages, msg, okFn, func() {})\n}\n"
  },
  {
    "path": "internal/view/cluster_info.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n)\n\nvar _ model.ClusterInfoListener = (*ClusterInfo)(nil)\n\n// ClusterInfo represents a cluster info view.\ntype ClusterInfo struct {\n\t*tview.Table\n\n\tapp    *App\n\tstyles *config.Styles\n}\n\n// NewClusterInfo returns a new cluster info view.\nfunc NewClusterInfo(app *App) *ClusterInfo {\n\treturn &ClusterInfo{\n\t\tTable:  tview.NewTable(),\n\t\tapp:    app,\n\t\tstyles: app.Styles,\n\t}\n}\n\n// Init initializes the view.\nfunc (c *ClusterInfo) Init() {\n\tc.SetBorderPadding(0, 0, 1, 0)\n\tc.app.Styles.AddListener(c)\n\tc.layout()\n\tc.StylesChanged(c.app.Styles)\n}\n\n// StylesChanged notifies skin changed.\nfunc (c *ClusterInfo) StylesChanged(s *config.Styles) {\n\tc.styles = s\n\tc.SetBackgroundColor(s.BgColor())\n\tc.updateStyle()\n}\n\nfunc (c *ClusterInfo) hasMetrics() bool {\n\tmx := c.app.Conn().HasMetrics()\n\tif mx {\n\t\tauth, err := c.app.Conn().CanI(\"\", client.NmxGVR, \"\", client.ListAccess)\n\t\tif err != nil {\n\t\t\tslog.Warn(\"No nodes metrics access\", slogs.Error, err)\n\t\t}\n\t\tmx = auth\n\t}\n\n\treturn mx\n}\n\nfunc (c *ClusterInfo) layout() {\n\tfor row, section := range []string{\"Context\", \"Cluster\", \"User\", \"K9s Rev\", \"K8s Rev\", \"CPU\", \"MEM\"} {\n\t\tc.SetCell(row, 0, c.sectionCell(section))\n\t\tc.SetCell(row, 1, c.infoCell(render.NAValue))\n\t}\n}\n\nfunc (c *ClusterInfo) sectionCell(t string) *tview.TableCell {\n\tcell := tview.NewTableCell(t + \":\")\n\tcell.SetAlign(tview.AlignLeft)\n\tcell.SetBackgroundColor(c.app.Styles.BgColor())\n\n\treturn cell\n}\n\nfunc (c *ClusterInfo) infoCell(t string) *tview.TableCell {\n\tcell := tview.NewTableCell(t)\n\tcell.SetExpansion(2)\n\tcell.SetTextColor(c.styles.K9s.Info.FgColor.Color())\n\tcell.SetBackgroundColor(c.app.Styles.BgColor())\n\n\treturn cell\n}\n\nfunc (c *ClusterInfo) setCell(row int, s string) int {\n\tif s == \"\" {\n\t\ts = render.NAValue\n\t}\n\tc.GetCell(row, 1).SetText(s)\n\treturn row + 1\n}\n\n// ClusterInfoUpdated notifies the cluster meta was updated.\nfunc (c *ClusterInfo) ClusterInfoUpdated(data *model.ClusterMeta) {\n\tc.ClusterInfoChanged(data, data)\n}\n\nfunc (*ClusterInfo) warnCell(s string, w bool) string {\n\tif w {\n\t\treturn fmt.Sprintf(\"[orangered::b]%s\", s)\n\t}\n\n\treturn s\n}\n\n// ClusterInfoChanged notifies the cluster meta was changed.\nfunc (c *ClusterInfo) ClusterInfoChanged(prev, curr *model.ClusterMeta) {\n\tc.app.QueueUpdateDraw(func() {\n\t\tc.Clear()\n\t\tc.layout()\n\n\t\tcontext := curr.Context\n\t\tif ic := ui.ROIndicator(c.app.Config.IsReadOnly(), c.app.Config.K9s.UI.NoIcons); ic != \"\" {\n\t\t\tcontext += \" \" + ic\n\t\t}\n\t\trow := c.setCell(0, context)\n\t\trow = c.setCell(row, curr.Cluster)\n\t\trow = c.setCell(row, curr.User)\n\t\tif curr.K9sLatest != \"\" {\n\t\t\trow = c.setCell(row, fmt.Sprintf(\"%s ⚡️[cadetblue::b]%s\", curr.K9sVer, curr.K9sLatest))\n\t\t} else {\n\t\t\trow = c.setCell(row, curr.K9sVer)\n\t\t}\n\t\trow = c.setCell(row, curr.K8sVer)\n\t\tif c.hasMetrics() {\n\t\t\trow = c.setCell(row, ui.AsPercDelta(prev.Cpu, curr.Cpu))\n\t\t\t_ = c.setCell(row, ui.AsPercDelta(prev.Mem, curr.Mem))\n\t\t\tc.setDefCon(curr.Cpu, curr.Mem)\n\t\t} else {\n\t\t\trow = c.setCell(row, c.warnCell(render.NAValue, true))\n\t\t\t_ = c.setCell(row, c.warnCell(render.NAValue, true))\n\t\t}\n\t\tc.updateStyle()\n\t})\n}\n\nconst defconFmt = \"%s %s level!\"\n\nfunc (c *ClusterInfo) setDefCon(cpu, mem int) {\n\tvar set bool\n\tl := c.app.Config.K9s.Thresholds.LevelFor(config.CPU, cpu)\n\tif l > config.SeverityLow {\n\t\tc.app.Status(flashLevel(l), fmt.Sprintf(defconFmt, flashMessage(l), \"CPU\"))\n\t\tset = true\n\t}\n\tl = c.app.Config.K9s.Thresholds.LevelFor(config.MEM, mem)\n\tif l > config.SeverityLow {\n\t\tc.app.Status(flashLevel(l), fmt.Sprintf(defconFmt, flashMessage(l), \"Memory\"))\n\t\tset = true\n\t}\n\tif !set && !c.app.IsBenchmarking() {\n\t\tc.app.ClearStatus(true)\n\t}\n}\n\nfunc (c *ClusterInfo) updateStyle() {\n\tfor row := range c.GetRowCount() {\n\t\tc.GetCell(row, 0).SetTextColor(c.styles.K9s.Info.FgColor.Color())\n\t\tc.GetCell(row, 0).SetBackgroundColor(c.styles.BgColor())\n\t\tvar s tcell.Style\n\t\ts = s.Bold(true)\n\t\ts = s.Foreground(c.styles.K9s.Info.SectionColor.Color())\n\t\ts = s.Background(c.styles.BgColor())\n\t\tc.GetCell(row, 1).SetStyle(s)\n\t}\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc flashLevel(l config.SeverityLevel) model.FlashLevel {\n\t//nolint:exhaustive\n\tswitch l {\n\tcase config.SeverityHigh:\n\t\treturn model.FlashErr\n\tcase config.SeverityMedium:\n\t\treturn model.FlashWarn\n\tdefault:\n\t\treturn model.FlashInfo\n\t}\n}\n\nfunc flashMessage(l config.SeverityLevel) string {\n\t//nolint:exhaustive\n\tswitch l {\n\tcase config.SeverityHigh:\n\t\treturn \"Critical\"\n\tcase config.SeverityMedium:\n\t\treturn \"Warning\"\n\tdefault:\n\t\treturn \"OK\"\n\t}\n}\n"
  },
  {
    "path": "internal/view/cm.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\n// ConfigMap represents a configmap viewer.\ntype ConfigMap struct {\n\tResourceViewer\n}\n\n// NewConfigMap returns a new viewer.\nfunc NewConfigMap(gvr *client.GVR) ResourceViewer {\n\ts := ConfigMap{\n\t\tResourceViewer: NewOwnerExtender(\n\t\t\tNewBrowser(gvr),\n\t\t),\n\t}\n\ts.AddBindKeysFn(s.bindKeys)\n\n\treturn &s\n}\n\nfunc (s *ConfigMap) bindKeys(aa *ui.KeyActions) {\n\taa.Add(ui.KeyU, ui.NewKeyAction(\"UsedBy\", s.refCmd, true))\n}\n\nfunc (s *ConfigMap) refCmd(evt *tcell.EventKey) *tcell.EventKey {\n\treturn scanRefs(evt, s.App(), s.GetTable(), client.CmGVR)\n}\n\nfunc scanRefs(evt *tcell.EventKey, a *App, t *Table, gvr *client.GVR) *tcell.EventKey {\n\tpath := t.GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\n\tctx := context.Background()\n\trefs, err := dao.ScanForRefs(refContext(gvr, path, true)(ctx), a.factory)\n\tif err != nil {\n\t\ta.Flash().Err(err)\n\t\treturn nil\n\t}\n\tif len(refs) == 0 {\n\t\ta.Flash().Warnf(\"No references found at this time for %s::%s. Check again later!\", gvr, path)\n\t\treturn nil\n\t}\n\ta.Flash().Infof(\"Viewing references for %s::%s\", gvr, path)\n\tview := NewReference(client.RefGVR)\n\tview.SetContextFn(refContext(gvr, path, false))\n\tif err := a.inject(view, false); err != nil {\n\t\ta.Flash().Err(err)\n\t}\n\n\treturn nil\n}\n\nfunc refContext(gvr *client.GVR, path string, wait bool) ContextFunc {\n\treturn func(ctx context.Context) context.Context {\n\t\tctx = context.WithValue(ctx, internal.KeyPath, path)\n\t\tctx = context.WithValue(ctx, internal.KeyGVR, gvr)\n\t\treturn context.WithValue(ctx, internal.KeyWait, wait)\n\t}\n}\n"
  },
  {
    "path": "internal/view/cm_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestConfigMapNew(t *testing.T) {\n\ts := view.NewConfigMap(client.CmGVR)\n\n\trequire.NoError(t, s.Init(makeCtx(t)))\n\tassert.Equal(t, \"ConfigMaps\", s.Name())\n\tassert.Len(t, s.Hints(), 9)\n}\n"
  },
  {
    "path": "internal/view/cmd/args.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage cmd\n\nimport (\n\t\"maps\"\n\t\"slices\"\n\t\"strings\"\n)\n\nconst (\n\tnsKey      = \"ns\"\n\ttopicKey   = \"topic\"\n\tfilterKey  = \"filter\"\n\tfuzzyKey   = \"fuzzy\"\n\tlabelKey   = \"labels\"\n\tcontextKey = \"context\"\n)\n\ntype args map[string]string\n\nfunc newArgs(p *Interpreter, aa []string) args {\n\targuments := make(args, len(aa))\n\tif len(aa) == 0 {\n\t\treturn arguments\n\t}\n\n\tfor i := 0; i < len(aa); i++ {\n\t\ta := strings.TrimSpace(aa[i])\n\t\tswitch {\n\t\tcase strings.Index(a, fuzzyFlag) == 0:\n\t\t\tif a == fuzzyFlag {\n\t\t\t\ti++\n\t\t\t\tif i < len(aa) {\n\t\t\t\t\targuments[fuzzyKey] = strings.ToLower(strings.TrimSpace(aa[i]))\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\targuments[fuzzyKey] = strings.ToLower(a[2:])\n\t\t\t}\n\n\t\tcase strings.Index(a, filterFlag) == 0:\n\t\t\tif p.IsDirCmd() {\n\t\t\t\tif _, ok := arguments[topicKey]; !ok {\n\t\t\t\t\targuments[topicKey] = a\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\targuments[filterKey] = strings.ToLower(a[1:])\n\t\t\t}\n\n\t\tcase strings.Index(a, contextFlag) == 0:\n\t\t\targuments[contextKey] = a[1:]\n\n\t\tcase isLabelArg(a):\n\t\t\targuments[labelKey] = strings.ToLower(a)\n\n\t\tdefault:\n\t\t\tswitch {\n\t\t\tcase p.IsContextCmd():\n\t\t\t\targuments[contextKey] = a\n\n\t\t\tcase p.IsDirCmd():\n\t\t\t\tif _, ok := arguments[topicKey]; !ok {\n\t\t\t\t\targuments[topicKey] = a\n\t\t\t\t}\n\n\t\t\tcase p.IsXrayCmd():\n\t\t\t\tif _, ok := arguments[topicKey]; ok {\n\t\t\t\t\targuments[nsKey] = strings.ToLower(a)\n\t\t\t\t} else {\n\t\t\t\t\targuments[topicKey] = strings.ToLower(a)\n\t\t\t\t}\n\n\t\t\tdefault:\n\t\t\t\targuments[nsKey] = strings.ToLower(a)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn arguments\n}\n\nfunc (a args) String() string {\n\tss := make([]string, 0, len(a))\n\tkk := maps.Keys(a)\n\tfor _, k := range slices.Sorted(kk) {\n\t\tv := a[k]\n\t\tswitch k {\n\t\tcase labelKey:\n\t\t\tv = \"'\" + v + \"'\"\n\t\tcase filterKey:\n\t\t\tv = filterFlag + v\n\t\tcase contextKey:\n\t\t\tv = contextFlag + v\n\t\t}\n\t\tss = append(ss, v)\n\t}\n\n\treturn strings.Join(ss, \" \")\n}\n\nfunc (a args) hasFilters() bool {\n\t_, fok := a[filterKey]\n\t_, zok := a[fuzzyKey]\n\t_, lok := a[labelKey]\n\n\treturn fok || zok || lok\n}\n\nfunc isLabelArg(arg string) bool {\n\tfor _, flag := range labelFlags {\n\t\tif strings.Contains(arg, flag) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "internal/view/cmd/args_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage cmd\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestFlagsNew(t *testing.T) {\n\tuu := map[string]struct {\n\t\ti  *Interpreter\n\t\taa []string\n\t\tll args\n\t}{\n\t\t\"empty\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\tll: make(args),\n\t\t},\n\n\t\t\"ns\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"ns1\"},\n\t\t\tll: args{nsKey: \"ns1\"},\n\t\t},\n\n\t\t\"ns+spaces\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\" ns1 \"},\n\t\t\tll: args{nsKey: \"ns1\"},\n\t\t},\n\n\t\t\"filter\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"/fred\"},\n\t\t\tll: args{filterKey: \"fred\"},\n\t\t},\n\n\t\t\"inverse-filter\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"/!fred\"},\n\t\t\tll: args{filterKey: \"!fred\"},\n\t\t},\n\n\t\t\"fuzzy-filter\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"-f\", \"fred\"},\n\t\t\tll: args{fuzzyKey: \"fred\"},\n\t\t},\n\n\t\t\"fuzzy-filter-nospace\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"-ffred\"},\n\t\t\tll: args{fuzzyKey: \"fred\"},\n\t\t},\n\n\t\t\"filter+ns\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"/fred\", \"  ns1 \"},\n\t\t\tll: args{nsKey: \"ns1\", filterKey: \"fred\"},\n\t\t},\n\n\t\t\"label\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"app=fred\"},\n\t\t\tll: args{labelKey: \"app=fred\"},\n\t\t},\n\n\t\t\"label-toast\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"=\"},\n\t\t\tll: args{labelKey: \"=\"},\n\t\t},\n\n\t\t\"multi-labels\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"app=fred,blee=duh\"},\n\t\t\tll: args{labelKey: \"app=fred,blee=duh\"},\n\t\t},\n\n\t\t\"label+ns\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"a=b,c=d\", \"  ns1  \"},\n\t\t\tll: args{labelKey: \"a=b,c=d\", nsKey: \"ns1\"},\n\t\t},\n\n\t\t\"full-monty\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"app=fred\", \"ns1\", \"-f\", \"blee\", \"/zorg\"},\n\t\t\tll: args{\n\t\t\t\tfilterKey: \"zorg\",\n\t\t\t\tfuzzyKey:  \"blee\",\n\t\t\t\tlabelKey:  \"app=fred\",\n\t\t\t\tnsKey:     \"ns1\",\n\t\t\t},\n\t\t},\n\n\t\t\"full-monty+ctx\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"app=fred\", \"ns1\", \"-f\", \"blee\", \"/zorg\", \"@ctx1\"},\n\t\t\tll: args{\n\t\t\t\tfilterKey:  \"zorg\",\n\t\t\t\tfuzzyKey:   \"blee\",\n\t\t\t\tlabelKey:   \"app=fred\",\n\t\t\t\tnsKey:      \"ns1\",\n\t\t\t\tcontextKey: \"ctx1\",\n\t\t\t},\n\t\t},\n\n\t\t\"full-monty+ctx-with-space\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"app=fred\", \"ns1\", \"-f\", \"blee\", \"/zorg\", \"@zorg fred\"},\n\t\t\tll: args{\n\t\t\t\tfilterKey:  \"zorg\",\n\t\t\t\tfuzzyKey:   \"blee\",\n\t\t\t\tlabelKey:   \"app=fred\",\n\t\t\t\tnsKey:      \"ns1\",\n\t\t\t\tcontextKey: \"zorg fred\",\n\t\t\t},\n\t\t},\n\n\t\t\"full-monty+ctx-first\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"@ctx1\", \"app=fred\", \"ns1\", \"-f\", \"blee\", \"/zorg\"},\n\t\t\tll: args{\n\t\t\t\tfilterKey:  \"zorg\",\n\t\t\t\tfuzzyKey:   \"blee\",\n\t\t\t\tlabelKey:   \"app=fred\",\n\t\t\t\tnsKey:      \"ns1\",\n\t\t\t\tcontextKey: \"ctx1\",\n\t\t\t},\n\t\t},\n\n\t\t\"full-monty+ctx-with-space-middle\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"app=fred\", \"@ctx1\", \"ns1\", \"-f\", \"blee\", \"/zorg\"},\n\t\t\tll: args{\n\t\t\t\tfilterKey:  \"zorg\",\n\t\t\t\tfuzzyKey:   \"blee\",\n\t\t\t\tlabelKey:   \"app=fred\",\n\t\t\t\tnsKey:      \"ns1\",\n\t\t\t\tcontextKey: \"ctx1\",\n\t\t\t},\n\t\t},\n\n\t\t\"caps\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"app=fred\", \"ns1\", \"-f\", \"blee\", \"/zorg\", \"@Dev\"},\n\t\t\tll: args{\n\t\t\t\tfilterKey:  \"zorg\",\n\t\t\t\tfuzzyKey:   \"blee\",\n\t\t\t\tlabelKey:   \"app=fred\",\n\t\t\t\tnsKey:      \"ns1\",\n\t\t\t\tcontextKey: \"Dev\",\n\t\t\t},\n\t\t},\n\n\t\t\"ctx\": {\n\t\t\ti:  NewInterpreter(\"ctx\"),\n\t\t\taa: []string{\"Dev\"},\n\t\t\tll: args{contextKey: \"Dev\"},\n\t\t},\n\n\t\t\"toast\": {\n\t\t\ti:  NewInterpreter(\"apply -f\"),\n\t\t\tll: args{},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tl := newArgs(u.i, u.aa)\n\t\t\tassert.Equal(t, u.ll, l)\n\t\t})\n\t}\n}\n\nfunc TestFlagsHasFilters(t *testing.T) {\n\tuu := map[string]struct {\n\t\ti  *Interpreter\n\t\taa []string\n\t\tok bool\n\t}{\n\t\t\"empty\": {},\n\t\t\"ns\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"ns1\"},\n\t\t},\n\t\t\"filter\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"/fred\"},\n\t\t\tok: true,\n\t\t},\n\t\t\"inverse-filter\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"/!fred\"},\n\t\t\tok: true,\n\t\t},\n\t\t\"fuzzy-filter\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"-f\", \"fred\"},\n\t\t\tok: true,\n\t\t},\n\t\t\"filter+ns\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"/fred\", \"ns1\"},\n\t\t\tok: true,\n\t\t},\n\t\t\"label\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"app=fred\"},\n\t\t\tok: true,\n\t\t},\n\t\t\"multi-labels\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"app=fred,blee=duh\"},\n\t\t\tok: true,\n\t\t},\n\t\t\"label+ns\": {\n\t\t\ti:  NewInterpreter(\"po\"),\n\t\t\taa: []string{\"app=fred\", \"ns1\"},\n\t\t\tok: true,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tl := newArgs(u.i, u.aa)\n\t\t\tassert.Equal(t, u.ok, l.hasFilters())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/view/cmd/helpers.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage cmd\n\nimport (\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n)\n\n// ToLabels converts a string into a map of labels.\nfunc ToLabels(s string) map[string]string {\n\tvar (\n\t\tll   = strings.Split(s, \",\")\n\t\tlbls = make(map[string]string, len(ll))\n\t)\n\tfor _, l := range ll {\n\t\tif k, v, ok := splitKv(l); ok {\n\t\t\tlbls[k] = v\n\t\t} else {\n\t\t\tcontinue\n\t\t}\n\t}\n\tif len(lbls) == 0 {\n\t\treturn nil\n\t}\n\n\treturn lbls\n}\n\nfunc splitKv(s string) (k, v string, ok bool) {\n\tswitch {\n\tcase strings.Contains(s, labelFlagNotEq):\n\t\tkv := strings.SplitN(s, labelFlagNotEq, 2)\n\t\tif len(kv) == 2 && kv[0] != \"\" && kv[1] != \"\" {\n\t\t\treturn strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1]), true\n\t\t}\n\tcase strings.Contains(s, labelFlagEqs):\n\t\tkv := strings.SplitN(s, labelFlagEqs, 2)\n\t\tif len(kv) == 2 && kv[0] != \"\" && kv[1] != \"\" {\n\t\t\treturn strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1]), true\n\t\t}\n\tcase strings.Contains(s, labelFlagEq):\n\t\tkv := strings.SplitN(s, labelFlagEq, 2)\n\t\tif len(kv) == 2 && kv[0] != \"\" && kv[1] != \"\" {\n\t\t\treturn strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1]), true\n\t\t}\n\t}\n\n\treturn \"\", \"\", false\n}\n\n// ShouldAddSuggest checks if a suggestion match the given command.\nfunc ShouldAddSuggest(command, suggest string) (string, bool) {\n\tif command != suggest && strings.HasPrefix(suggest, command) {\n\t\treturn strings.TrimPrefix(suggest, command), true\n\t}\n\n\treturn \"\", false\n}\n\n// SuggestSubCommand suggests namespaces or contexts based on current command.\nfunc SuggestSubCommand(command string, namespaces client.NamespaceNames, contexts []string) []string {\n\tp := NewInterpreter(command)\n\tvar suggests []string\n\tswitch {\n\tcase p.IsCowCmd(), p.IsHelpCmd(), p.IsAliasCmd(), p.IsBailCmd(), p.IsDirCmd():\n\t\treturn nil\n\n\tcase p.IsXrayCmd():\n\t\t_, ns, ok := p.XrayArgs()\n\t\tif !ok || ns == \"\" {\n\t\t\treturn nil\n\t\t}\n\t\tsuggests = completeNS(ns, namespaces)\n\n\tcase p.IsContextCmd():\n\t\tn, ok := p.ContextArg()\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\t\tsuggests = completeCtx(command, n, contexts)\n\n\tcase p.HasNS():\n\t\tif n, ok := p.HasContext(); ok {\n\t\t\tsuggests = completeCtx(command, n, contexts)\n\t\t}\n\t\tif len(suggests) > 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tns, ok := p.NSArg()\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\t\tsuggests = completeNS(ns, namespaces)\n\n\tdefault:\n\t\tif n, ok := p.HasContext(); ok {\n\t\t\tsuggests = completeCtx(command, n, contexts)\n\t\t}\n\t}\n\tslices.Sort(suggests)\n\n\treturn suggests\n}\n\nfunc completeNS(s string, nn client.NamespaceNames) []string {\n\ts = strings.ToLower(s)\n\tvar suggests []string\n\tif suggest, ok := ShouldAddSuggest(s, client.NamespaceAll); ok {\n\t\tsuggests = append(suggests, suggest)\n\t}\n\tfor ns := range nn {\n\t\tif suggest, ok := ShouldAddSuggest(s, ns); ok {\n\t\t\tsuggests = append(suggests, suggest)\n\t\t}\n\t}\n\n\treturn suggests\n}\n\nfunc completeCtx(command, s string, contexts []string) []string {\n\tvar suggests []string\n\tfor _, ctxName := range contexts {\n\t\tif suggest, ok := ShouldAddSuggest(s, ctxName); ok {\n\t\t\tif s == \"\" && !strings.HasSuffix(command, \" \") {\n\t\t\t\tsuggests = append(suggests, \" \"+suggest)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tsuggests = append(suggests, suggest)\n\t\t}\n\t}\n\n\treturn suggests\n}\n"
  },
  {
    "path": "internal/view/cmd/helpers_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage cmd\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc init() {\n\tslog.SetDefault(slog.New(slog.DiscardHandler))\n}\n\nfunc Test_toLabels(t *testing.T) {\n\tuu := map[string]struct {\n\t\ts  string\n\t\tll map[string]string\n\t}{\n\t\t\"empty\": {},\n\t\t\"toast\": {\n\t\t\ts: \"=\",\n\t\t},\n\t\t\"toast-1\": {\n\t\t\ts: \"=,\",\n\t\t},\n\t\t\"toast-2\": {\n\t\t\ts: \",\",\n\t\t},\n\t\t\"toast-3\": {\n\t\t\ts: \",=\",\n\t\t},\n\t\t\"simple\": {\n\t\t\ts:  \"a=b\",\n\t\t\tll: map[string]string{\"a\": \"b\"},\n\t\t},\n\t\t\"multi\": {\n\t\t\ts:  \"a=b,c=d\",\n\t\t\tll: map[string]string{\"a\": \"b\", \"c\": \"d\"},\n\t\t},\n\t\t\"multi-toast1\": {\n\t\t\ts:  \"a=,c=d\",\n\t\t\tll: map[string]string{\"c\": \"d\"},\n\t\t},\n\t\t\"multi-toast2\": {\n\t\t\ts:  \"a=b,=d\",\n\t\t\tll: map[string]string{\"a\": \"b\"},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.ll, ToLabels(u.s))\n\t\t})\n\t}\n}\n\nfunc TestSuggestSubCommand(t *testing.T) {\n\tnamespaceNames := map[string]struct{}{\n\t\t\"kube-system\":   {},\n\t\t\"kube-public\":   {},\n\t\t\"default\":       {},\n\t\t\"nginx-ingress\": {},\n\t}\n\tcontextNames := []string{\"develop\", \"test\", \"pre\", \"prod\"}\n\n\ttests := []struct {\n\t\tCommand     string\n\t\tSuggestions []string\n\t}{\n\t\t{Command: \"q\", Suggestions: nil},\n\t\t{Command: \"xray  dp\", Suggestions: nil},\n\t\t{Command: \"help  k\", Suggestions: nil},\n\t\t{Command: \"ctx p\", Suggestions: []string{\"re\", \"rod\"}},\n\t\t{Command: \"ctx   p\", Suggestions: []string{\"re\", \"rod\"}},\n\t\t{Command: \"ctx pr\", Suggestions: []string{\"e\", \"od\"}},\n\t\t{Command: \"ctx\", Suggestions: []string{\" develop\", \" pre\", \" prod\", \" test\"}},\n\t\t{Command: \"ctx \", Suggestions: []string{\"develop\", \"pre\", \"prod\", \"test\"}},\n\t\t{Command: \"context   d\", Suggestions: []string{\"evelop\"}},\n\t\t{Command: \"contexts   t\", Suggestions: []string{\"est\"}},\n\t\t{Command: \"po \", Suggestions: nil},\n\t\t{Command: \"po  x\", Suggestions: nil},\n\t\t{Command: \"po k\", Suggestions: []string{\"ube-public\", \"ube-system\"}},\n\t\t{Command: \"po  kube-\", Suggestions: []string{\"public\", \"system\"}},\n\t}\n\n\tfor _, tt := range tests {\n\t\tgot := SuggestSubCommand(tt.Command, namespaceNames, contextNames)\n\t\tassert.Equal(t, tt.Suggestions, got)\n\t}\n}\n"
  },
  {
    "path": "internal/view/cmd/interpreter.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage cmd\n\nimport (\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n)\n\n// Interpreter tracks user prompt input.\ntype Interpreter struct {\n\tline    string\n\tcmd     string\n\taliases []string\n\targs    args\n}\n\n// NewInterpreter returns a new instance.\nfunc NewInterpreter(s string, aliases ...string) *Interpreter {\n\tc := Interpreter{\n\t\tline:    s,\n\t\targs:    make(args),\n\t\taliases: aliases,\n\t}\n\tc.grok()\n\n\treturn &c\n}\n\n// ClearNS clears the current namespace if any.\nfunc (c *Interpreter) ClearNS() {\n\tc.SwitchNS(client.BlankNamespace)\n}\n\n// SwitchNS replaces the current namespace with the provided one.\nfunc (c *Interpreter) SwitchNS(ns string) {\n\tif ons, ok := c.NSArg(); ok && ons != client.BlankNamespace {\n\t\tc.Reset(strings.TrimSpace(strings.Replace(c.line, \" \"+ons, \" \"+ns, 1)), \"\")\n\t\treturn\n\t}\n\tif ns != client.BlankNamespace {\n\t\tc.Reset(strings.TrimSpace(c.line)+\" \"+ns, \"\")\n\t}\n}\n\nfunc (c *Interpreter) Merge(p *Interpreter) {\n\tif p == nil {\n\t\treturn\n\t}\n\tc.cmd = p.cmd\n\tfor k, v := range p.args {\n\t\tc.args[k] = v\n\t}\n\tc.line = c.cmd + \" \" + c.args.String()\n}\n\nfunc (c *Interpreter) grok() {\n\tff := strings.Fields(c.line)\n\tif len(ff) == 0 {\n\t\treturn\n\t}\n\tc.cmd = strings.ToLower(ff[0])\n\n\tvar lbls string\n\tline := strings.TrimSpace(strings.Replace(c.line, ff[0], \"\", 1))\n\tif strings.Contains(line, \"'\") {\n\t\tstart, end, ok := quoteIndicies(line)\n\t\tif ok {\n\t\t\tlbls = line[start+1 : end]\n\t\t\tline = strings.TrimSpace(strings.Replace(line, \"'\"+lbls+\"'\", \"\", 1))\n\t\t} else {\n\t\t\tslog.Error(\"Unmatched single quote in command line\", slogs.Line, c.line)\n\t\t\tline = \"\"\n\t\t}\n\t}\n\tff = strings.Fields(line)\n\tif lbls != \"\" {\n\t\tff = append(ff, lbls)\n\t}\n\tc.args = newArgs(c, ff)\n}\n\nfunc quoteIndicies(s string) (start, end int, ok bool) {\n\tstart, end = -1, -1\n\tfor i, r := range s {\n\t\tif r == '\\'' {\n\t\t\tif start == -1 {\n\t\t\t\tstart = i\n\t\t\t} else if end == -1 {\n\t\t\t\tend = i\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\tok = start != -1 && end != -1\n\treturn\n}\n\n// HasNS returns true if ns is present in prompt.\nfunc (c *Interpreter) HasNS() bool {\n\tns, ok := c.args[nsKey]\n\n\treturn ok && ns != \"\"\n}\n\n// Cmd returns the command.\nfunc (c *Interpreter) Cmd() string {\n\treturn c.cmd\n}\n\nfunc (c *Interpreter) Aliases() []string {\n\treturn c.aliases\n}\n\nfunc (c *Interpreter) Args() string {\n\treturn strings.TrimSpace(strings.Replace(c.line, c.cmd, \"\", 1))\n}\n\n// IsBlank returns true if prompt is empty.\nfunc (c *Interpreter) IsBlank() bool {\n\treturn c.line == \"\"\n}\n\n// Amend merges prompts.\nfunc (c *Interpreter) Amend(c1 *Interpreter) {\n\tc.cmd = c1.cmd\n\tif c.args == nil {\n\t\tc.args = make(args, len(c1.args))\n\t}\n\tfor k, v := range c1.args {\n\t\tif v != \"\" {\n\t\t\tc.args[k] = v\n\t\t}\n\t}\n}\n\n// Reset resets with new command.\nfunc (c *Interpreter) Reset(line, alias string) *Interpreter {\n\tc.line = line\n\tc.grok()\n\n\tif alias != \"\" && alias != c.cmd {\n\t\tc.addAlias(alias)\n\t}\n\n\treturn c\n}\n\nfunc (c *Interpreter) addAlias(a string) {\n\tfor _, v := range c.aliases {\n\t\tif v == a {\n\t\t\treturn\n\t\t}\n\t}\n\tc.aliases = append(c.aliases, a)\n}\n\n// GetLine returns the prompt.\nfunc (c *Interpreter) GetLine() string {\n\treturn strings.TrimSpace(c.line)\n}\n\n// IsCowCmd returns true if cow cmd is detected.\nfunc (c *Interpreter) IsCowCmd() bool {\n\treturn c.cmd == cowCmd\n}\n\n// IsHelpCmd returns true if help cmd is detected.\nfunc (c *Interpreter) IsHelpCmd() bool {\n\treturn helpCmd.Has(c.cmd)\n}\n\n// IsBailCmd returns true if quit cmd is detected.\nfunc (c *Interpreter) IsBailCmd() bool {\n\treturn bailCmd.Has(c.cmd)\n}\n\n// IsAliasCmd returns true if alias cmd is detected.\nfunc (c *Interpreter) IsAliasCmd() bool {\n\treturn aliasCmd.Has(c.cmd)\n}\n\n// IsXrayCmd returns true if xray cmd is detected.\nfunc (c *Interpreter) IsXrayCmd() bool {\n\treturn xrayCmd.Has(c.cmd)\n}\n\n// IsContextCmd returns true if context cmd is detected.\nfunc (c *Interpreter) IsContextCmd() bool {\n\treturn contextCmd.Has(c.cmd)\n}\n\n// IsNamespaceCmd returns true if ns cmd is detected.\nfunc (c *Interpreter) IsNamespaceCmd() bool {\n\treturn namespaceCmd.Has(c.cmd)\n}\n\n// IsDirCmd returns true if dir cmd is detected.\nfunc (c *Interpreter) IsDirCmd() bool {\n\treturn dirCmd.Has(c.cmd)\n}\n\n// IsRBACCmd returns true if rbac cmd is detected.\nfunc (c *Interpreter) IsRBACCmd() bool {\n\treturn c.cmd == canCmd\n}\n\n// ContextArg returns context cmd arg.\nfunc (c *Interpreter) ContextArg() (string, bool) {\n\tif c.IsContextCmd() || strings.Contains(c.line, contextFlag) {\n\t\treturn c.args[contextKey], true\n\t}\n\n\treturn \"\", false\n}\n\n// ResetContextArg deletes context arg.\nfunc (c *Interpreter) ResetContextArg() {\n\tdelete(c.args, contextFlag)\n}\n\n// DirArg returns the directory is present.\nfunc (c *Interpreter) DirArg() (string, bool) {\n\tif !c.IsDirCmd() {\n\t\treturn \"\", false\n\t}\n\td, ok := c.args[topicKey]\n\n\treturn d, ok && d != \"\"\n}\n\n// CowArg returns the cow message.\nfunc (c *Interpreter) CowArg() (string, bool) {\n\tif !c.IsCowCmd() {\n\t\treturn \"\", false\n\t}\n\tm, ok := c.args[nsKey]\n\n\treturn m, ok && m != \"\"\n}\n\n// RBACArgs returns the subject and topic is any.\nfunc (c *Interpreter) RBACArgs() (subject, verb string, ok bool) {\n\tif !c.IsRBACCmd() {\n\t\treturn\n\t}\n\ttt := rbacRX.FindStringSubmatch(c.line)\n\tif len(tt) < 3 {\n\t\treturn\n\t}\n\tsubject, verb, ok = tt[1], tt[2], true\n\n\treturn\n}\n\n// XrayArgs return the gvr and ns if any.\nfunc (c *Interpreter) XrayArgs() (cmd, namespace string, ok bool) {\n\tif !c.IsXrayCmd() {\n\t\treturn\n\t}\n\tgvr, ok1 := c.args[topicKey]\n\tif !ok1 {\n\t\treturn\n\t}\n\n\tns, ok2 := c.args[nsKey]\n\tswitch {\n\tcase ok1 && ok2:\n\t\tcmd, namespace, ok = gvr, ns, true\n\tcase ok1 && !ok2:\n\t\tcmd, namespace, ok = gvr, \"\", true\n\tdefault:\n\t\treturn\n\t}\n\n\treturn\n}\n\n// FilterArg returns the current filter if any.\nfunc (c *Interpreter) FilterArg() (string, bool) {\n\tf, ok := c.args[filterKey]\n\n\treturn f, ok && f != \"\"\n}\n\n// FuzzyArg returns the fuzzy filter if any.\nfunc (c *Interpreter) FuzzyArg() (string, bool) {\n\tf, ok := c.args[fuzzyKey]\n\n\treturn f, ok && f != \"\"\n}\n\n// NSArg returns the current ns if any.\nfunc (c *Interpreter) NSArg() (string, bool) {\n\tns, ok := c.args[nsKey]\n\n\treturn ns, ok && ns != client.BlankNamespace\n}\n\n// HasContext returns the current context if any.\nfunc (c *Interpreter) HasContext() (string, bool) {\n\tctx, ok := c.args[contextKey]\n\n\treturn ctx, ok && ctx != \"\"\n}\n\n// LabelsSelector returns the label selector if any.\nfunc (c *Interpreter) LabelsSelector() (labels.Selector, error) {\n\treturn labels.Parse(c.args[labelKey])\n}\n"
  },
  {
    "path": "internal/view/cmd/interpreter_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage cmd_test\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRbacCmd(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcmd  string\n\t\tok   bool\n\t\targs []string\n\t}{\n\t\t\"empty\": {},\n\t\t\"user\": {\n\t\t\tcmd:  \"can u:fernand\",\n\t\t\tok:   true,\n\t\t\targs: []string{\"u\", \"fernand\"},\n\t\t},\n\t\t\"user_spacing\": {\n\t\t\tcmd:  \"can   u:  fernand  \",\n\t\t\tok:   true,\n\t\t\targs: []string{\"u\", \"fernand\"},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tp := cmd.NewInterpreter(u.cmd)\n\t\t\tassert.Equal(t, u.ok, p.IsRBACCmd())\n\n\t\t\tc, s, ok := p.RBACArgs()\n\t\t\tassert.Equal(t, u.ok, ok)\n\t\t\tif u.ok {\n\t\t\t\tassert.Equal(t, u.args[0], c)\n\t\t\t\tassert.Equal(t, u.args[1], s)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestNsCmd(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcmd string\n\t\tok  bool\n\t\tns  string\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"happy\": {\n\t\t\tcmd: \"pod fred\",\n\t\t\tok:  true,\n\t\t\tns:  \"fred\",\n\t\t},\n\n\t\t\"ns-arg-spaced\": {\n\t\t\tcmd: \"pod      fred   \",\n\t\t\tok:  true,\n\t\t\tns:  \"fred\",\n\t\t},\n\n\t\t\"caps-no-ns\": {\n\t\t\tcmd: \"Deploy\",\n\t\t},\n\n\t\t\"caps-with-ns\": {\n\t\t\tcmd: \"DEPLOY Fred\",\n\t\t\tok:  true,\n\t\t\tns:  \"fred\",\n\t\t},\n\n\t\t\"no-ns\": {\n\t\t\tcmd: \"pod\",\n\t\t},\n\n\t\t\"full-ns\": {\n\t\t\tcmd: \"pod app=blee fred @zorg\",\n\t\t\tok:  true,\n\t\t\tns:  \"fred\",\n\t\t},\n\n\t\t\"full-repeat-ns\": {\n\t\t\tcmd: \"pod app=blee blee @zorg\",\n\t\t\tok:  true,\n\t\t\tns:  \"blee\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tp := cmd.NewInterpreter(u.cmd)\n\t\t\tns, ok := p.NSArg()\n\t\t\tassert.Equal(t, u.ok, ok)\n\t\t\tif u.ok {\n\t\t\t\tassert.Equal(t, u.ns, ns)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSwitchNS(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcmd string\n\t\tns  string\n\t\te   string\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"blank\": {\n\t\t\tcmd: \"pod fred\",\n\t\t\te:   \"pod\",\n\t\t},\n\n\t\t\"no-op\": {\n\t\t\tcmd: \"pod fred\",\n\t\t\tns:  \"fred\",\n\t\t\te:   \"pod fred\",\n\t\t},\n\n\t\t\"no-ns\": {\n\t\t\tcmd: \"pod\",\n\t\t\tns:  \"blee\",\n\t\t\te:   \"pod blee\",\n\t\t},\n\n\t\t\"full-ns\": {\n\t\t\tcmd: \"pod app=blee fred @zorg\",\n\t\t\tns:  \"blee\",\n\t\t\te:   \"pod app=blee blee @zorg\",\n\t\t},\n\n\t\t\"full--repeat-ns\": {\n\t\t\tcmd: \"pod app=zorg zorg @zorg\",\n\t\t\tns:  \"blee\",\n\t\t\te:   \"pod app=zorg blee @zorg\",\n\t\t},\n\n\t\t\"full-no-ns\": {\n\t\t\tcmd: \"pod app=blee @zorg\",\n\t\t\tns:  \"blee\",\n\t\t\te:   \"pod app=blee @zorg blee\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tp := cmd.NewInterpreter(u.cmd)\n\t\t\tp.SwitchNS(u.ns)\n\t\t\tassert.Equal(t, u.e, p.GetLine())\n\t\t})\n\t}\n}\n\nfunc TestClearNS(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcmd string\n\t\te   string\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"has-ns\": {\n\t\t\tcmd: \"pod fred\",\n\t\t\te:   \"pod\",\n\t\t},\n\n\t\t\"no-ns\": {\n\t\t\tcmd: \"pod\",\n\t\t\te:   \"pod\",\n\t\t},\n\n\t\t\"full-repeat-ns\": {\n\t\t\tcmd: \"pod app=blee @zorg zorg\",\n\t\t\te:   \"pod app=blee @zorg\",\n\t\t},\n\n\t\t\"full-no-ns\": {\n\t\t\tcmd: \"pod app=blee @zorg\",\n\t\t\te:   \"pod app=blee @zorg\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tp := cmd.NewInterpreter(u.cmd)\n\t\t\tp.ClearNS()\n\t\t\tassert.Equal(t, u.e, p.GetLine())\n\t\t})\n\t}\n}\n\nfunc TestFilterCmd(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcmd    string\n\t\tok     bool\n\t\tfilter string\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"normal\": {\n\t\t\tcmd:    \"pod /fred\",\n\t\t\tok:     true,\n\t\t\tfilter: \"fred\",\n\t\t},\n\n\t\t\"caps\": {\n\t\t\tcmd:    \"POD /FRED\",\n\t\t\tok:     true,\n\t\t\tfilter: \"fred\",\n\t\t},\n\n\t\t\"filter+ns\": {\n\t\t\tcmd:    \"pod /fred ns1\",\n\t\t\tok:     true,\n\t\t\tfilter: \"fred\",\n\t\t},\n\n\t\t\"ns+filter\": {\n\t\t\tcmd:    \"pod ns1 /fred\",\n\t\t\tok:     true,\n\t\t\tfilter: \"fred\",\n\t\t},\n\n\t\t\"ns+filter+labels\": {\n\t\t\tcmd:    \"pod ns1 /fred app=blee,fred=zorg\",\n\t\t\tok:     true,\n\t\t\tfilter: \"fred\",\n\t\t},\n\n\t\t\"filtered\": {\n\t\t\tcmd:    \"pod /cilium kube-system\",\n\t\t\tok:     true,\n\t\t\tfilter: \"cilium\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tp := cmd.NewInterpreter(u.cmd)\n\t\t\tf, ok := p.FilterArg()\n\t\t\tassert.Equal(t, u.ok, ok)\n\t\t\tif u.ok {\n\t\t\t\tassert.Equal(t, u.filter, f)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLabelCmd(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcmd    string\n\t\terr    error\n\t\tlabels string\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"plain\": {\n\t\t\tcmd:    \"pod fred=blee\",\n\t\t\tlabels: \"fred=blee\",\n\t\t},\n\n\t\t\"multi\": {\n\t\t\tcmd:    \"pod fred=blee,zorg=duh\",\n\t\t\tlabels: \"fred=blee,zorg=duh\",\n\t\t},\n\n\t\t\"complex-lbls\": {\n\t\t\tcmd:    \"pod 'fred in (blee,zorg),blee notin (zorg)'\",\n\t\t\tlabels: \"blee notin (zorg),fred in (blee,zorg)\",\n\t\t},\n\n\t\t\"no-lbls\": {\n\t\t\tcmd: \"pod ns-1\",\n\t\t},\n\n\t\t\"multi-ns\": {\n\t\t\tcmd:    \"pod fred=blee,zorg=duh ns1\",\n\t\t\tlabels: \"fred=blee,zorg=duh\",\n\t\t},\n\n\t\t\"l-arg-spaced\": {\n\t\t\tcmd:    \"pod   fred=blee   \",\n\t\t\tlabels: \"fred=blee\",\n\t\t},\n\n\t\t\"l-arg-caps\": {\n\t\t\tcmd:    \"POD  FRED=BLEE   \",\n\t\t\tlabels: \"fred=blee\",\n\t\t},\n\n\t\t\"toast-labels\": {\n\t\t\tcmd: \"pod =blee\",\n\t\t\terr: errors.New(\"found '=', expected: !, identifier, or 'end of string'\"),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tp := cmd.NewInterpreter(u.cmd)\n\t\t\tll, err := p.LabelsSelector()\n\t\t\tassert.Equal(t, u.err, err)\n\t\t\tif err == nil {\n\t\t\t\tassert.Equal(t, u.labels, ll.String())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestXRayCmd(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcmd     string\n\t\tok      bool\n\t\tres, ns string\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"happy\": {\n\t\t\tcmd: \"xray po\",\n\t\t\tok:  true,\n\t\t\tres: \"po\",\n\t\t},\n\n\t\t\"happy+ns\": {\n\t\t\tcmd: \"xray po ns1\",\n\t\t\tok:  true,\n\t\t\tres: \"po\",\n\t\t\tns:  \"ns1\",\n\t\t},\n\n\t\t\"toast\": {\n\t\t\tcmd: \"xrayzor po\",\n\t\t},\n\n\t\t\"toast-1\": {\n\t\t\tcmd: \"xray\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tp := cmd.NewInterpreter(u.cmd)\n\t\t\tres, ns, ok := p.XrayArgs()\n\t\t\tassert.Equal(t, u.ok, ok)\n\t\t\tif u.ok {\n\t\t\t\tassert.Equal(t, u.res, res)\n\t\t\t\tassert.Equal(t, u.ns, ns)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestDirCmd(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcmd string\n\t\tok  bool\n\t\tdir string\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"happy\": {\n\t\t\tcmd: \"dir dir1\",\n\t\t\tok:  true,\n\t\t\tdir: \"dir1\",\n\t\t},\n\n\t\t\"extra-ns\": {\n\t\t\tcmd: \"dir dir1 ns1\",\n\t\t\tok:  true,\n\t\t\tdir: \"dir1\",\n\t\t},\n\n\t\t\"toast\": {\n\t\t\tcmd: \"dirdel dir1\",\n\t\t},\n\n\t\t\"toast-nodir\": {\n\t\t\tcmd: \"dir\",\n\t\t},\n\n\t\t\"caps\": {\n\t\t\tcmd: \"dir DirName\",\n\t\t\tok:  true,\n\t\t\tdir: \"DirName\",\n\t\t},\n\n\t\t\"abs\": {\n\t\t\tcmd: \"dir /tmp\",\n\t\t\tok:  true,\n\t\t\tdir: \"/tmp\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tp := cmd.NewInterpreter(u.cmd)\n\t\t\tdir, ok := p.DirArg()\n\t\t\tassert.Equal(t, u.ok, ok)\n\t\t\tassert.Equal(t, u.dir, dir)\n\t\t})\n\t}\n}\n\nfunc TestRBACCmd(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcmd      string\n\t\tok       bool\n\t\tcat, sub string\n\t}{\n\t\t\"empty\": {},\n\t\t\"toast\": {\n\t\t\tcmd: \"canopy u:bozo\",\n\t\t},\n\t\t\"toast-1\": {\n\t\t\tcmd: \"can u:\",\n\t\t},\n\t\t\"toast-2\": {\n\t\t\tcmd: \"can bozo\",\n\t\t},\n\t\t\"user\": {\n\t\t\tcmd: \"can u:bozo\",\n\t\t\tok:  true,\n\t\t\tcat: \"u\",\n\t\t\tsub: \"bozo\",\n\t\t},\n\t\t\"group\": {\n\t\t\tcmd: \"can g:bozo\",\n\t\t\tok:  true,\n\t\t\tcat: \"g\",\n\t\t\tsub: \"bozo\",\n\t\t},\n\t\t\"sa\": {\n\t\t\tcmd: \"can s:bozo\",\n\t\t\tok:  true,\n\t\t\tcat: \"s\",\n\t\t\tsub: \"bozo\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tp := cmd.NewInterpreter(u.cmd)\n\t\t\tcat, sub, ok := p.RBACArgs()\n\t\t\tassert.Equal(t, u.ok, ok)\n\t\t\tif u.ok {\n\t\t\t\tassert.Equal(t, u.cat, cat)\n\t\t\t\tassert.Equal(t, u.sub, sub)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestContextCmd(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcmd string\n\t\tok  bool\n\t\tctx string\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"happy-full\": {\n\t\t\tcmd: \"context ctx1\",\n\t\t\tok:  true,\n\t\t\tctx: \"ctx1\",\n\t\t},\n\n\t\t\"happy-alias\": {\n\t\t\tcmd: \"ctx ctx1\",\n\t\t\tok:  true,\n\t\t\tctx: \"ctx1\",\n\t\t},\n\n\t\t\"toast\": {\n\t\t\tcmd: \"ctxto ctx1\",\n\t\t},\n\n\t\t\"caps\": {\n\t\t\tcmd: \"ctx Dev\",\n\t\t\tok:  true,\n\t\t\tctx: \"Dev\",\n\t\t},\n\n\t\t\"contains-key\": {\n\t\t\tcmd: \"ctx kind-fred\",\n\t\t\tok:  true,\n\t\t\tctx: \"kind-fred\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tp := cmd.NewInterpreter(u.cmd)\n\t\t\tassert.Equal(t, u.ok, p.IsContextCmd())\n\t\t\tif u.ok {\n\t\t\t\tctx, ok := p.ContextArg()\n\t\t\t\tassert.Equal(t, u.ok, ok)\n\t\t\t\tassert.Equal(t, u.ctx, ctx)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHelpCmd(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcmd string\n\t\tok  bool\n\t}{\n\t\t\"empty\": {},\n\t\t\"plain\": {\n\t\t\tcmd: \"help\",\n\t\t\tok:  true,\n\t\t},\n\t\t\"toast\": {\n\t\t\tcmd: \"helpme\",\n\t\t},\n\t\t\"toast1\": {\n\t\t\tcmd: \"hozer\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tp := cmd.NewInterpreter(u.cmd)\n\t\t\tassert.Equal(t, u.ok, p.IsHelpCmd())\n\t\t})\n\t}\n}\n\nfunc TestBailCmd(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcmd string\n\t\tok  bool\n\t}{\n\t\t\"empty\": {},\n\t\t\"plain\": {\n\t\t\tcmd: \"quit\",\n\t\t\tok:  true,\n\t\t},\n\t\t\"q\": {\n\t\t\tcmd: \"q\",\n\t\t\tok:  true,\n\t\t},\n\t\t\"q!\": {\n\t\t\tcmd: \"q!\",\n\t\t\tok:  true,\n\t\t},\n\t\t\"toast\": {\n\t\t\tcmd: \"zorg\",\n\t\t},\n\t\t\"toast1\": {\n\t\t\tcmd: \"quitter\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tp := cmd.NewInterpreter(u.cmd)\n\t\t\tassert.Equal(t, u.ok, p.IsBailCmd())\n\t\t})\n\t}\n}\n\nfunc TestAliasCmd(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcmd string\n\t\tok  bool\n\t}{\n\t\t\"empty\": {},\n\t\t\"plain\": {\n\t\t\tcmd: \"alias\",\n\t\t\tok:  true,\n\t\t},\n\t\t\"a\": {\n\t\t\tcmd: \"a\",\n\t\t\tok:  true,\n\t\t},\n\t\t\"toast\": {\n\t\t\tcmd: \"abba\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tp := cmd.NewInterpreter(u.cmd)\n\t\t\tassert.Equal(t, u.ok, p.IsAliasCmd())\n\t\t})\n\t}\n}\n\nfunc TestCowCmd(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcmd string\n\t\tok  bool\n\t}{\n\t\t\"empty\": {},\n\t\t\"plain\": {\n\t\t\tcmd: \"cow\",\n\t\t\tok:  true,\n\t\t},\n\t\t\"msg\": {\n\t\t\tcmd: \"cow bumblebeetuna\",\n\t\t\tok:  true,\n\t\t},\n\t\t\"toast\": {\n\t\t\tcmd: \"cowdy\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tp := cmd.NewInterpreter(u.cmd)\n\t\t\tassert.Equal(t, u.ok, p.IsCowCmd())\n\t\t})\n\t}\n}\n\nfunc TestArgs(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcmd string\n\t\tok  bool\n\t\tctx string\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"with-plain-context\": {\n\t\t\tcmd: \"po @fred\",\n\t\t\tok:  true,\n\t\t\tctx: \"fred\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tp := cmd.NewInterpreter(u.cmd)\n\t\t\tctx, ok := p.ContextArg()\n\t\t\tassert.Equal(t, u.ok, ok)\n\t\t\tif u.ok {\n\t\t\t\tassert.Equal(t, u.ctx, ctx)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_grokLabels(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcmd  string\n\t\terr  error\n\t\tlbls string\n\t}{\n\t\t\"empty\": {},\n\n\t\t\"no-labels\": {\n\t\t\tcmd: \"po @fred\",\n\t\t},\n\n\t\t\"plain-label\": {\n\t\t\tcmd:  \"po a=b,b=c @fred\",\n\t\t\tlbls: \"a=b,b=c\",\n\t\t},\n\n\t\t\"label-quotes\": {\n\t\t\tcmd:  \"po 'a=b,b=c' @fred\",\n\t\t\tlbls: \"a=b,b=c\",\n\t\t},\n\n\t\t\"partial-quotes-label\": {\n\t\t\tcmd:  \"po 'a=b @fred\",\n\t\t\tlbls: \"\",\n\t\t},\n\n\t\t\"complex\": {\n\t\t\tcmd:  \"po 'a in (b,c),b notin (c,z)' fred'\",\n\t\t\tlbls: \"a in (b,c),b notin (c,z)\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tp := cmd.NewInterpreter(u.cmd)\n\t\t\tsel, err := p.LabelsSelector()\n\t\t\tassert.Equal(t, u.err, err)\n\t\t\tif err == nil {\n\t\t\t\tassert.Equal(t, u.lbls, sel.String())\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/view/cmd/types.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage cmd\n\nimport (\n\t\"regexp\"\n\n\t\"k8s.io/apimachinery/pkg/util/sets\"\n)\n\nconst (\n\tcowCmd         = \"cow\"\n\tcanCmd         = \"can\"\n\tnsFlag         = \"-n\"\n\tfilterFlag     = \"/\"\n\tlabelFlagEq    = \"=\"\n\tlabelFlagEqs   = \"==\"\n\tlabelFlagNotEq = \"!=\"\n\tlabelFlagIn    = \" in \"\n\tlabelFlagNotin = \" notin \"\n\tlabelFlagQuote = \"'\"\n\tlabel\n\tfuzzyFlag   = \"-f\"\n\tcontextFlag = \"@\"\n)\n\nvar (\n\tlabelFlags = []string{\n\t\tlabelFlagEq,\n\t\tlabelFlagEqs,\n\t\tlabelFlagNotEq,\n\t\tlabelFlagIn,\n\t\tlabelFlagNotin,\n\t}\n\trbacRX = regexp.MustCompile(`^can\\s+([ugs]):\\s*([\\w-:]+)\\s*$`)\n\n\tcontextCmd = sets.New(\n\t\t\"ctx\",\n\t\t\"context\",\n\t\t\"contexts\",\n\t)\n\tnamespaceCmd = sets.New(\n\t\t\"ns\",\n\t\t\"namespace\",\n\t\t\"namespaces\",\n\t)\n\tdirCmd = sets.New(\n\t\t\"dir\",\n\t\t\"dirs\",\n\t\t\"d\",\n\t\t\"ls\",\n\t)\n\tbailCmd = sets.New(\n\t\t\"q\",\n\t\t\"q!\",\n\t\t\"qa\",\n\t\t\"Q\",\n\t\t\"quit\",\n\t\t\"exit\",\n\t)\n\thelpCmd = sets.New(\n\t\t\"?\",\n\t\t\"h\",\n\t\t\"help\",\n\t)\n\taliasCmd = sets.New(\n\t\t\"a\",\n\t\t\"alias\",\n\t\t\"aliases\",\n\t)\n\txrayCmd = sets.New(\n\t\t\"x\",\n\t\t\"xr\",\n\t\t\"xray\",\n\t)\n)\n"
  },
  {
    "path": "internal/view/command.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"regexp\"\n\t\"runtime/debug\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/util/sets\"\n)\n\nconst (\n\tpodCmd = \"v1/pods\"\n\tctxCmd = \"ctx\"\n)\n\nvar (\n\tcustomViewers MetaViewers\n\tcontextRX     = regexp.MustCompile(`\\s+@([\\w-]+)`)\n)\n\n// Command represents a user command.\ntype Command struct {\n\tapp   *App\n\talias *dao.Alias\n\tmx    sync.Mutex\n}\n\n// NewCommand returns a new command.\nfunc NewCommand(app *App) *Command {\n\treturn &Command{\n\t\tapp: app,\n\t}\n}\n\n// AliasesFor gather all known aliases for a given resource.\nfunc (c *Command) AliasesFor(gvr *client.GVR) sets.Set[string] {\n\tif c.alias == nil {\n\t\treturn sets.New[string]()\n\t}\n\treturn c.alias.AliasesFor(gvr)\n}\n\n// Init initializes the command.\nfunc (c *Command) Init(path string) error {\n\tif c.app.factory != nil {\n\t\tc.alias = dao.NewAlias(c.app.factory)\n\t\tif _, err := c.alias.Ensure(path); err != nil {\n\t\t\tslog.Error(\"Ensure aliases failed\", slogs.Error, err)\n\t\t\treturn err\n\t\t}\n\t}\n\tcustomViewers = loadCustomViewers()\n\n\treturn nil\n}\n\n// Reset resets Command and reload aliases.\nfunc (c *Command) Reset(path string, nuke bool) error {\n\tc.mx.Lock()\n\tdefer c.mx.Unlock()\n\n\tif c.alias == nil {\n\t\treturn nil\n\t}\n\n\tif nuke {\n\t\tc.alias.Clear()\n\t}\n\tif _, err := c.alias.Ensure(path); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nvar allowedCmds = sets.New[*client.GVR](\n\tclient.PodGVR,\n\tclient.SvcGVR,\n\tclient.DpGVR,\n\tclient.DsGVR,\n\tclient.StsGVR,\n\tclient.RsGVR,\n)\n\nfunc allowedXRay(gvr *client.GVR) bool {\n\treturn allowedCmds.Has(gvr)\n}\n\nfunc (c *Command) contextCmd(p *cmd.Interpreter, pushCmd bool) error {\n\tct, ok := p.ContextArg()\n\tif !ok {\n\t\treturn fmt.Errorf(\"invalid command use `context xxx`\")\n\t}\n\n\tif ct != \"\" {\n\t\treturn useContext(c.app, ct)\n\t}\n\n\tgvr, v, comd, err := c.viewMetaFor(p)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif comd != nil {\n\t\tp = comd\n\t}\n\n\treturn c.exec(p, gvr, c.componentFor(gvr, ct, v), true, pushCmd)\n}\n\nfunc (*Command) namespaceCmd(p *cmd.Interpreter) bool {\n\tns, ok := p.NSArg()\n\tif !ok {\n\t\treturn false\n\t}\n\n\tif ns != \"\" {\n\t\t_ = p.Reset(client.PodGVR.String(), \"\")\n\t\tp.SwitchNS(ns)\n\t}\n\n\treturn false\n}\n\nfunc (c *Command) aliasCmd(p *cmd.Interpreter, pushCmd bool) error {\n\tfilter, _ := p.FilterArg()\n\n\tv := NewAlias(client.AliGVR)\n\tv.SetFilter(filter, true)\n\n\treturn c.exec(p, client.AliGVR, v, false, pushCmd)\n}\n\nfunc (c *Command) xrayCmd(p *cmd.Interpreter, pushCmd bool) error {\n\targ, cns, ok := p.XrayArgs()\n\tif !ok {\n\t\treturn errors.New(\"invalid command. use `xray xxx`\")\n\t}\n\tif c.alias == nil {\n\t\treturn fmt.Errorf(\"no connection available\")\n\t}\n\tgvr, ok := c.alias.Resolve(cmd.NewInterpreter(arg))\n\tif !ok {\n\t\treturn fmt.Errorf(\"invalid resource name: %q\", arg)\n\t}\n\tif !allowedXRay(gvr) {\n\t\treturn fmt.Errorf(\"unsupported resource %q\", arg)\n\t}\n\tns := c.app.Config.ActiveNamespace()\n\tif cns != \"\" {\n\t\tns = cns\n\t}\n\tif err := c.app.Config.SetActiveNamespace(client.CleanseNamespace(ns)); err != nil {\n\t\treturn err\n\t}\n\tif err := c.app.switchNS(ns); err != nil {\n\t\treturn err\n\t}\n\n\treturn c.exec(p, client.XGVR, NewXray(gvr), true, pushCmd)\n}\n\n// Run execs the command by showing associated display.\nfunc (c *Command) run(p *cmd.Interpreter, fqn string, clearStack, pushCmd bool) error {\n\tif c.specialCmd(p, pushCmd) {\n\t\treturn nil\n\t}\n\tgvr, v, comd, err := c.viewMetaFor(p)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif comd != nil {\n\t\tp.Merge(comd)\n\t}\n\n\tif context, ok := p.HasContext(); ok {\n\t\tif context != c.app.Config.ActiveContextName() {\n\t\t\tif err := c.app.Config.Save(true); err != nil {\n\t\t\t\tslog.Error(\"Config save failed during command exec\", slogs.Error, err)\n\t\t\t} else {\n\t\t\t\tslog.Debug(\"Successfully saved config\", slogs.Context, context)\n\t\t\t}\n\t\t}\n\t\tres, err := dao.AccessorFor(c.app.factory, client.CtGVR)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tswitcher, ok := res.(dao.Switchable)\n\t\tif !ok {\n\t\t\treturn errors.New(\"expecting a switchable resource\")\n\t\t}\n\t\tif err := switcher.Switch(context); err != nil {\n\t\t\tslog.Error(\"Unable to switch context\", slogs.Error, err)\n\t\t\treturn err\n\t\t}\n\t\tif err := c.app.switchContext(p, false); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tns := c.app.Config.ActiveNamespace()\n\tif cns, ok := p.NSArg(); ok {\n\t\tns = cns\n\t}\n\tif ok, err := dao.MetaAccess.IsNamespaced(gvr); ok && err == nil {\n\t\tif err := c.app.switchNS(ns); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.SwitchNS(ns)\n\t} else {\n\t\tp.ClearNS()\n\t}\n\n\tco := c.componentFor(gvr, fqn, v)\n\tco.SetFilter(\"\", true)\n\tco.SetLabelSelector(labels.Everything(), true)\n\tif f, ok := p.FilterArg(); ok {\n\t\tco.SetFilter(f, true)\n\t}\n\tif f, ok := p.FuzzyArg(); ok {\n\t\tco.SetFilter(\"-f \"+f, true)\n\t}\n\tif sel, err := p.LabelsSelector(); err == nil {\n\t\tco.SetLabelSelector(sel, false)\n\t} else {\n\t\tslog.Error(\"Unable to grok labels selector\", slogs.Error, err)\n\t}\n\n\treturn c.exec(p, gvr, co, clearStack, pushCmd)\n}\n\nfunc (c *Command) defaultCmd(isRoot bool) error {\n\tif c.app.Conn() == nil || !c.app.Conn().ConnectionOK() {\n\t\treturn c.run(cmd.NewInterpreter(\"context\"), \"\", true, true)\n\t}\n\n\tdefCmd := podCmd\n\tif isRoot {\n\t\tdefCmd = ctxCmd\n\t}\n\tp := cmd.NewInterpreter(c.app.Config.ActiveView())\n\tif p.IsBlank() {\n\t\treturn c.run(p.Reset(defCmd, \"\"), \"\", true, true)\n\t}\n\n\tif err := c.run(p, \"\", true, true); err != nil {\n\t\tslog.Error(\"Command exec failed. Using default command\",\n\t\t\tslogs.Command, p.GetLine(),\n\t\t\tslogs.Error, err,\n\t\t)\n\t\tp = p.Reset(defCmd, \"\")\n\t\treturn c.run(p, \"\", true, true)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Command) specialCmd(p *cmd.Interpreter, pushCmd bool) bool {\n\tswitch {\n\tcase p.IsCowCmd():\n\t\tif msg, ok := p.CowArg(); !ok {\n\t\t\tc.app.Flash().Errf(\"Invalid command. Use `cow xxx`\")\n\t\t} else {\n\t\t\tc.app.cowCmd(msg)\n\t\t}\n\tcase p.IsBailCmd():\n\t\tc.app.BailOut(0)\n\tcase p.IsHelpCmd():\n\t\t_ = c.app.helpCmd(nil)\n\tcase p.IsAliasCmd():\n\t\tif err := c.aliasCmd(p, pushCmd); err != nil {\n\t\t\tc.app.Flash().Err(err)\n\t\t}\n\tcase p.IsXrayCmd():\n\t\tif err := c.xrayCmd(p, pushCmd); err != nil {\n\t\t\tc.app.Flash().Err(err)\n\t\t}\n\tcase p.IsRBACCmd():\n\t\tif cat, sub, ok := p.RBACArgs(); !ok {\n\t\t\tc.app.Flash().Errf(\"Invalid command. Use `can [u|g|s]:xxx`\")\n\t\t} else if err := c.app.inject(NewPolicy(c.app, cat, sub), true); err != nil {\n\t\t\tc.app.Flash().Err(err)\n\t\t}\n\tcase p.IsContextCmd():\n\t\tif err := c.contextCmd(p, pushCmd); err != nil {\n\t\t\tc.app.Flash().Err(err)\n\t\t}\n\tcase p.IsNamespaceCmd():\n\t\treturn c.namespaceCmd(p)\n\tcase p.IsDirCmd():\n\t\tif a, ok := p.DirArg(); !ok {\n\t\t\tc.app.Flash().Errf(\"Invalid command. Use `dir xxx`\")\n\t\t} else if err := c.app.dirCmd(a, pushCmd); err != nil {\n\t\t\tc.app.Flash().Err(err)\n\t\t}\n\tdefault:\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc (c *Command) viewMetaFor(p *cmd.Interpreter) (*client.GVR, *MetaViewer, *cmd.Interpreter, error) {\n\tif c.alias == nil {\n\t\treturn client.NoGVR, nil, nil, fmt.Errorf(\"no connection available\")\n\t}\n\tgvr, ok := c.alias.Resolve(p)\n\tif !ok {\n\t\treturn client.NoGVR, nil, nil, fmt.Errorf(\"`%s` command not found\", p.Cmd())\n\t}\n\n\tv := MetaViewer{\n\t\tviewerFn: func(gvr *client.GVR) ResourceViewer {\n\t\t\treturn NewScaleExtender(NewOwnerExtender(NewBrowser(gvr)))\n\t\t},\n\t}\n\tif mv, ok := customViewers[gvr]; ok {\n\t\tv = mv\n\t}\n\n\treturn gvr, &v, p, nil\n}\n\nfunc (*Command) componentFor(gvr *client.GVR, fqn string, v *MetaViewer) ResourceViewer {\n\tvar view ResourceViewer\n\tif v.viewerFn != nil {\n\t\tview = v.viewerFn(gvr)\n\t} else {\n\t\tview = NewBrowser(gvr)\n\t}\n\n\tview.SetInstance(fqn)\n\tif v.enterFn != nil {\n\t\tview.GetTable().SetEnterFn(v.enterFn)\n\t}\n\n\treturn view\n}\n\nfunc (c *Command) exec(p *cmd.Interpreter, gvr *client.GVR, comp model.Component, clearStack, pushCmd bool) (err error) {\n\tdefer func() {\n\t\tif e := recover(); e != nil {\n\t\t\tslog.Error(\"Failure detected during command exec\", slogs.Error, e)\n\t\t\tc.app.Content.Dump()\n\t\t\tslog.Debug(\"Dumping history buffer\", slogs.CmdHist, c.app.cmdHistory.List())\n\t\t\tslog.Error(\"Dumping stack\", slogs.Stack, string(debug.Stack()))\n\n\t\t\tci := cmd.NewInterpreter(podCmd)\n\t\t\tcurrentCommand, ok := c.app.cmdHistory.Top()\n\t\t\tif ok {\n\t\t\t\tci = ci.Reset(currentCommand, \"\")\n\t\t\t}\n\t\t\terr = c.run(ci, \"\", true, true)\n\t\t}\n\t}()\n\n\tif comp == nil {\n\t\treturn fmt.Errorf(\"no component found for %s\", gvr)\n\t}\n\tcomp.SetCommand(p)\n\n\tif clearStack {\n\t\tv := contextRX.ReplaceAllString(p.GetLine(), \"\")\n\t\tc.app.Config.SetActiveView(v)\n\t}\n\tif err := c.app.inject(comp, clearStack); err != nil {\n\t\treturn err\n\t}\n\tif pushCmd {\n\t\tc.app.cmdHistory.Push(p.GetLine())\n\t}\n\tslog.Debug(\"History (exec)\", slogs.Stack, strings.Join(c.app.cmdHistory.List(), \"|\"))\n\n\treturn\n}\n"
  },
  {
    "path": "internal/view/command_test.go",
    "content": "package view\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_viewMetaFor(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcmd string\n\t\tgvr *client.GVR\n\t\tp   *cmd.Interpreter\n\t\terr error\n\t}{\n\t\t\"empty\": {\n\t\t\tcmd: \"\",\n\t\t\tgvr: client.PodGVR,\n\t\t\terr: errors.New(\"`` command not found\"),\n\t\t},\n\n\t\t\"toast\": {\n\t\t\tcmd: \"v1/pd\",\n\t\t\tgvr: client.PodGVR,\n\t\t\terr: errors.New(\"`v1/pd` command not found\"),\n\t\t},\n\n\t\t\"gvr\": {\n\t\t\tcmd: \"v1/pods\",\n\t\t\tgvr: client.PodGVR,\n\t\t\tp:   cmd.NewInterpreter(\"v1/pods\"),\n\t\t\terr: errors.New(\"blah\"),\n\t\t},\n\n\t\t\"short-name\": {\n\t\t\tcmd: \"po\",\n\t\t\tgvr: client.PodGVR,\n\t\t\tp:   cmd.NewInterpreter(\"v1/pods\", \"po\"),\n\t\t\terr: errors.New(\"blee\"),\n\t\t},\n\n\t\t\"custom-alias\": {\n\t\t\tcmd: \"pdl\",\n\t\t\tgvr: client.PodGVR,\n\t\t\tp:   cmd.NewInterpreter(\"v1/pods @fred 'app=blee' default\", \"pdl\"),\n\t\t\terr: errors.New(\"blee\"),\n\t\t},\n\n\t\t\"inception\": {\n\t\t\tcmd: \"pdal blee\",\n\t\t\tgvr: client.PodGVR,\n\t\t\tp:   cmd.NewInterpreter(\"v1/pods @fred 'app=blee' blee\", \"pdal\", \"pod\"),\n\t\t\terr: errors.New(\"blee\"),\n\t\t},\n\t}\n\n\tc := &Command{\n\t\talias: &dao.Alias{\n\t\t\tAliases: config.NewAliases(),\n\t\t},\n\t}\n\tc.alias.Define(client.PodGVR, \"po\", \"pod\", \"pods\", client.PodGVR.String())\n\tc.alias.Define(client.NewGVR(\"pod default\"), \"pd\")\n\tc.alias.Define(client.NewGVR(\"pod @fred 'app=blee' default\"), \"pdl\")\n\tc.alias.Define(client.NewGVR(\"pdl\"), \"pdal\")\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tp := cmd.NewInterpreter(u.cmd)\n\t\t\tgvr, _, acmd, err := c.viewMetaFor(p)\n\t\t\tif err != nil {\n\t\t\t\tassert.Equal(t, u.err.Error(), err.Error())\n\t\t\t} else {\n\t\t\t\tassert.Equal(t, u.gvr, gvr)\n\t\t\t\tassert.Equal(t, u.p, acmd)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/view/container.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/port\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n\tv1 \"k8s.io/api/core/v1\"\n)\n\nconst containerTitle = \"Containers\"\n\n// Container represents a container view.\ntype Container struct {\n\tResourceViewer\n}\n\n// NewContainer returns a new container view.\nfunc NewContainer(gvr *client.GVR) ResourceViewer {\n\tc := Container{}\n\tc.ResourceViewer = NewLogsExtender(NewBrowser(gvr), c.logOptions)\n\tc.SetEnvFn(c.k9sEnv)\n\tc.GetTable().SetEnterFn(c.viewLogs)\n\tc.GetTable().SetDecorateFn(c.decorateRows)\n\tc.GetTable().SetSortCol(\"IDX\", true)\n\tc.AddBindKeysFn(c.bindKeys)\n\tc.GetTable().SetDecorateFn(c.portForwardIndicator)\n\n\treturn &c\n}\n\nfunc (c *Container) portForwardIndicator(data *model1.TableData) {\n\tff := c.App().factory.Forwarders()\n\tcol, ok := data.IndexOfHeader(\"PF\")\n\tif !ok {\n\t\treturn\n\t}\n\tdata.RowsRange(func(_ int, re model1.RowEvent) bool {\n\t\tif ff.IsContainerForwarded(c.GetTable().Path, re.Row.ID) {\n\t\t\tre.Row.Fields[col] = \"[orange::b]Ⓕ\"\n\t\t}\n\t\treturn true\n\t})\n}\n\nfunc (c *Container) decorateRows(data *model1.TableData) {\n\tdecorateCpuMemHeaderRows(c.App(), data)\n}\n\n// Name returns the component name.\nfunc (*Container) Name() string { return containerTitle }\n\nfunc (c *Container) bindDangerousKeys(aa *ui.KeyActions) {\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyS: ui.NewKeyActionWithOpts(\n\t\t\t\"Shell\",\n\t\t\tc.shellCmd,\n\t\t\tui.ActionOpts{\n\t\t\t\tVisible:   true,\n\t\t\t\tDangerous: true,\n\t\t\t}),\n\t\tui.KeyA: ui.NewKeyActionWithOpts(\n\t\t\t\"Attach\",\n\t\t\tc.attachCmd,\n\t\t\tui.ActionOpts{\n\t\t\t\tVisible:   true,\n\t\t\t\tDangerous: true,\n\t\t\t}),\n\t})\n}\n\nfunc (c *Container) bindKeys(aa *ui.KeyActions) {\n\taa.Delete(tcell.KeyCtrlSpace, ui.KeySpace)\n\n\tif !c.App().Config.IsReadOnly() {\n\t\tc.bindDangerousKeys(aa)\n\t}\n\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyF:      ui.NewKeyAction(\"Show PortForward\", c.showPFCmd, true),\n\t\tui.KeyShiftF: ui.NewKeyAction(\"PortForward\", c.portFwdCmd, true),\n\t})\n}\n\nfunc (c *Container) k9sEnv() Env {\n\tpath := c.GetTable().GetSelectedItem()\n\trow := c.GetTable().GetSelectedRow(path)\n\tenv := defaultEnv(c.App().Conn().Config(), path, c.GetTable().GetModel().Peek().Header(), row)\n\tenv[\"NAMESPACE\"], env[\"POD\"] = client.Namespaced(c.GetTable().Path)\n\n\treturn env\n}\n\nfunc (c *Container) logOptions(prev bool) (*dao.LogOptions, error) {\n\tpath := c.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn nil, errors.New(\"nothing selected\")\n\t}\n\n\tcfg := c.App().Config.K9s.Logger\n\topts := dao.LogOptions{\n\t\tPath:            c.GetTable().Path,\n\t\tContainer:       path,\n\t\tLines:           cfg.TailCount,\n\t\tSinceSeconds:    cfg.SinceSeconds,\n\t\tSingleContainer: true,\n\t\tShowTimestamp:   cfg.ShowTime,\n\t\tPrevious:        prev,\n\t}\n\n\treturn &opts, nil\n}\n\nfunc (c *Container) viewLogs(*App, ui.Tabular, *client.GVR, string) {\n\tc.ResourceViewer.(*LogsExtender).showLogs(c.GetTable().Path, false)\n}\n\n// Handlers...\n\nfunc (c *Container) showPFCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := c.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\n\tif !c.App().factory.Forwarders().IsContainerForwarded(c.GetTable().Path, path) {\n\t\tc.App().Flash().Errf(\"no port-forward defined\")\n\t\treturn nil\n\t}\n\tpf := NewPortForward(client.PfGVR)\n\tpf.SetContextFn(c.portForwardContext)\n\tif err := c.App().inject(pf, false); err != nil {\n\t\tc.App().Flash().Err(err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Container) portForwardContext(ctx context.Context) context.Context {\n\tif bc := c.App().BenchFile; bc != \"\" {\n\t\tctx = context.WithValue(ctx, internal.KeyBenchCfg, c.App().BenchFile)\n\t}\n\n\treturn context.WithValue(ctx, internal.KeyPath, c.GetTable().Path)\n}\n\nfunc (c *Container) shellCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := c.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\n\tvar err error\n\tc.Stop()\n\tdefer func() {\n\t\tc.Start()\n\t\tif err != nil {\n\t\t\tc.App().QueueUpdate(func() {\n\t\t\t\tif err != nil {\n\t\t\t\t\tc.App().Flash().Errf(\"Shell exec failed: %s\", err)\n\t\t\t\t}\n\t\t\t})\n\n\t\t\tc.App().Flash().Err(err)\n\t\t}\n\t}()\n\terr = shellIn(c.App(), c.GetTable().Path, path)\n\n\treturn nil\n}\n\nfunc (c *Container) attachCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tsel := c.GetTable().GetSelectedItem()\n\tif sel == \"\" {\n\t\treturn evt\n\t}\n\n\tc.Stop()\n\tdefer c.Start()\n\tattachIn(c.App(), c.GetTable().Path, sel)\n\n\treturn nil\n}\n\nfunc (c *Container) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := c.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\n\tif _, ok := c.App().factory.ForwarderFor(fwFQN(c.GetTable().Path, path)); ok {\n\t\tc.App().Flash().Errf(\"A port-forward already exists on container %s\", c.GetTable().Path)\n\t\treturn nil\n\t}\n\n\tports, ann, ok := c.listForwardable(path)\n\tif !ok {\n\t\treturn nil\n\t}\n\tShowPortForwards(c, c.GetTable().Path+\"|\"+path, ports, ann, startFwdCB)\n\n\treturn nil\n}\n\nfunc checkRunningStatus(co string, ss []v1.ContainerStatus) error {\n\tvar cs *v1.ContainerStatus\n\tfor i := range ss {\n\t\tif ss[i].Name == co {\n\t\t\tcs = &ss[i]\n\t\t\tbreak\n\t\t}\n\t}\n\tif cs == nil {\n\t\treturn fmt.Errorf(\"unable to locate container status for %q\", co)\n\t}\n\n\tif render.ToContainerState(cs.State) != \"Running\" {\n\t\treturn fmt.Errorf(\"Container %s is not running?\", co)\n\t}\n\n\treturn nil\n}\n\nfunc locateContainer(co string, cc []v1.Container) (*v1.Container, error) {\n\tfor i := range cc {\n\t\tif cc[i].Name == co {\n\t\t\treturn &cc[i], nil\n\t\t}\n\t}\n\treturn nil, fmt.Errorf(\"unable to locate container named %q\", co)\n}\n\nfunc (c *Container) listForwardable(path string) (port.ContainerPortSpecs, map[string]string, bool) {\n\tpo, err := fetchPod(c.App().factory, c.GetTable().Path)\n\tif err != nil {\n\t\treturn nil, nil, false\n\t}\n\n\tco, err := locateContainer(path, po.Spec.Containers)\n\tif err != nil {\n\t\tc.App().Flash().Err(err)\n\t\treturn nil, nil, false\n\t}\n\n\tif err := checkRunningStatus(path, po.Status.ContainerStatuses); err != nil {\n\t\tc.App().Flash().Err(err)\n\t\treturn nil, nil, false\n\t}\n\n\treturn port.FromContainerPorts(path, co.Ports), po.Annotations, true\n}\n"
  },
  {
    "path": "internal/view/container_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestContainerNew(t *testing.T) {\n\tc := view.NewContainer(client.CoGVR)\n\n\trequire.NoError(t, c.Init(makeCtx(t)))\n\tassert.Equal(t, \"Containers\", c.Name())\n\tassert.Len(t, c.Hints(), 13)\n}\n"
  },
  {
    "path": "internal/view/context.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/ui/dialog\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n)\n\nconst (\n\trenamePage = \"rename\"\n\tinputField = \"New name:\"\n)\n\n// Context presents a context viewer.\ntype Context struct {\n\tResourceViewer\n}\n\n// NewContext returns a new viewer.\nfunc NewContext(gvr *client.GVR) ResourceViewer {\n\tc := Context{\n\t\tResourceViewer: NewBrowser(gvr),\n\t}\n\tc.GetTable().SetEnterFn(c.useCtx)\n\tc.AddBindKeysFn(c.bindKeys)\n\n\treturn &c\n}\n\nfunc (c *Context) bindKeys(aa *ui.KeyActions) {\n\taa.Delete(ui.KeyShiftA, tcell.KeyCtrlSpace, ui.KeySpace)\n\tif !c.App().Config.IsReadOnly() {\n\t\tc.bindDangerousKeys(aa)\n\t}\n}\n\nfunc (c *Context) bindDangerousKeys(aa *ui.KeyActions) {\n\taa.Add(ui.KeyR, ui.NewKeyAction(\"Rename\", c.renameCmd, true))\n\taa.Add(tcell.KeyCtrlD, ui.NewKeyAction(\"Delete\", c.deleteCmd, true))\n}\n\nfunc (c *Context) renameCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tcontextName := c.GetTable().GetSelectedItem()\n\tif contextName == \"\" {\n\t\treturn evt\n\t}\n\n\tc.showRenameModal(contextName, c.renameDialogCallback)\n\n\treturn nil\n}\n\nfunc (c *Context) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tcontextName := c.GetTable().GetSelectedItem()\n\tif contextName == \"\" {\n\t\treturn evt\n\t}\n\n\td := c.App().Styles.Dialog()\n\tdialog.ShowConfirm(&d, c.App().Content.Pages, \"Delete\", fmt.Sprintf(\"Delete context %q?\", contextName), func() {\n\t\tif err := c.App().factory.Client().Config().DelContext(contextName); err != nil {\n\t\t\tc.App().Flash().Err(err)\n\t\t\treturn\n\t\t}\n\t\tc.Refresh()\n\t}, func() {})\n\n\treturn nil\n}\n\nfunc (c *Context) renameDialogCallback(form *tview.Form, contextName string) error {\n\tapp := c.App()\n\tinput := form.GetFormItemByLabel(inputField).(*tview.InputField)\n\tif err := app.factory.Client().Config().RenameContext(contextName, input.GetText()); err != nil {\n\t\tc.App().Flash().Err(err)\n\t\treturn nil\n\t}\n\tc.Refresh()\n\treturn nil\n}\n\nfunc (c *Context) showRenameModal(name string, ok func(form *tview.Form, contextName string) error) {\n\tapp := c.App()\n\tstyles := app.Styles.Dialog()\n\n\tf := tview.NewForm().\n\t\tSetItemPadding(0).\n\t\tSetButtonsAlign(tview.AlignCenter).\n\t\tSetButtonBackgroundColor(styles.ButtonBgColor.Color()).\n\t\tSetButtonTextColor(styles.ButtonFgColor.Color()).\n\t\tSetLabelColor(styles.LabelFgColor.Color()).\n\t\tSetFieldTextColor(styles.FieldFgColor.Color())\n\tf.AddInputField(inputField, name, 0, nil, nil).\n\t\tAddButton(\"OK\", func() {\n\t\t\tif err := ok(f, name); err != nil {\n\t\t\t\tapp.Flash().Err(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tapp.Content.Pages.RemovePage(renamePage)\n\t\t}).\n\t\tAddButton(\"Cancel\", func() {\n\t\t\tapp.Content.RemovePage(renamePage)\n\t\t})\n\n\tm := tview.NewModalForm(\"<Rename>\", f)\n\tm.SetText(fmt.Sprintf(\"Rename context %q?\", name))\n\tm.SetDoneFunc(func(int, string) {\n\t\tapp.Content.RemovePage(renamePage)\n\t})\n\tapp.Content.AddPage(renamePage, m, false, false)\n\tapp.Content.ShowPage(renamePage)\n\n\tfor i := range f.GetButtonCount() {\n\t\tf.GetButton(i).\n\t\t\tSetBackgroundColorActivated(styles.ButtonFocusBgColor.Color()).\n\t\t\tSetLabelColorActivated(styles.ButtonFocusFgColor.Color())\n\t}\n}\n\nfunc (c *Context) useCtx(app *App, _ ui.Tabular, gvr *client.GVR, path string) {\n\tslog.Debug(\"Using context\",\n\t\tslogs.GVR, gvr,\n\t\tslogs.FQN, path,\n\t)\n\tif err := useContext(app, path); err != nil {\n\t\tapp.Flash().Err(err)\n\t\treturn\n\t}\n\tc.App().clearHistory()\n\tc.Refresh()\n\tc.GetTable().Select(1, 0)\n}\n\nfunc useContext(app *App, name string) error {\n\tif app.Content.Top() != nil {\n\t\tapp.Content.Top().Stop()\n\t}\n\tres, err := dao.AccessorFor(app.factory, client.CtGVR)\n\tif err != nil {\n\t\treturn err\n\t}\n\tswitcher, ok := res.(dao.Switchable)\n\tif !ok {\n\t\treturn errors.New(\"expecting a switchable resource\")\n\t}\n\n\tapp.Config.K9s.ToggleContextSwitch(true)\n\tdefer app.Config.K9s.ToggleContextSwitch(false)\n\n\t// Save config prior to context switch...\n\tif err := app.Config.Save(true); err != nil {\n\t\tslog.Error(\"Fail to save config to disk\", slogs.Subsys, \"config\", slogs.Error, err)\n\t}\n\n\tif err := switcher.Switch(name); err != nil {\n\t\tslog.Error(\"Context switch failed during use command\", slogs.Error, err)\n\t\treturn err\n\t}\n\n\treturn app.switchContext(cmd.NewInterpreter(\"ctx \"+name), true)\n}\n"
  },
  {
    "path": "internal/view/context_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestContext(t *testing.T) {\n\tctx := view.NewContext(client.CtGVR)\n\n\trequire.NoError(t, ctx.Init(makeCtx(t)))\n\tassert.Equal(t, \"Contexts\", ctx.Name())\n\tassert.Len(t, ctx.Hints(), 8)\n}\n"
  },
  {
    "path": "internal/view/cow.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n)\n\n// Cow represents a bomb viewer.\ntype Cow struct {\n\t*tview.TextView\n\n\tactions *ui.KeyActions\n\tapp     *App\n\tsays    string\n}\n\n// NewCow returns a have a cow viewer.\nfunc NewCow(app *App, says string) *Cow {\n\treturn &Cow{\n\t\tTextView: tview.NewTextView(),\n\t\tapp:      app,\n\t\tactions:  ui.NewKeyActions(),\n\t\tsays:     says,\n\t}\n}\n\n// Init initializes the viewer.\nfunc (c *Cow) Init(_ context.Context) error {\n\tc.SetBorder(true)\n\tc.SetScrollable(true).SetWrap(true).SetRegions(true)\n\tc.SetDynamicColors(true)\n\tc.SetHighlightColor(tcell.ColorOrange)\n\tc.SetTitleColor(tcell.ColorAqua)\n\tc.SetInputCapture(c.keyboard)\n\tc.SetBorderPadding(0, 0, 1, 1)\n\tc.updateTitle()\n\tc.SetTextAlign(tview.AlignCenter)\n\n\tc.app.Styles.AddListener(c)\n\tc.StylesChanged(c.app.Styles)\n\n\tc.bindKeys()\n\tc.SetInputCapture(c.keyboard)\n\tc.talk()\n\n\treturn nil\n}\n\n// InCmdMode checks if prompt is active.\nfunc (*Cow) InCmdMode() bool {\n\treturn false\n}\n\nfunc (c *Cow) talk() {\n\tsays := c.says\n\tif says == \"\" {\n\t\tsays = \"Nothing to report here. Please move along...\"\n\t}\n\tx, _, w, _ := c.GetRect()\n\tc.SetText(cowTalk(says, (x+w)/2))\n}\n\nfunc cowTalk(says string, w int) string {\n\tmsg := fmt.Sprintf(\"[red::]< [::b]Ruroh? %s[::-] >\", says)\n\tbuff := make([]string, 0, len(cow)+3)\n\tbuff = append(buff,\n\t\t\"[red::] \"+strings.Repeat(\"─\", len(says)+8),\n\t\tstrings.TrimSuffix(msg, \"\\n\"),\n\t\t\" \"+strings.Repeat(\"─\", len(says)+8),\n\t)\n\trCount := w/2 - 8\n\tif rCount < 0 {\n\t\trCount = w / 2\n\t}\n\tspacer := strings.Repeat(\" \", rCount)\n\tfor _, s := range cow {\n\t\tbuff = append(buff, \"[red::b]\"+spacer+s)\n\t}\n\treturn strings.Join(buff, \"\\n\")\n}\n\nfunc (c *Cow) bindKeys() {\n\tc.actions.Add(tcell.KeyEscape, ui.NewKeyAction(\"Back\", c.resetCmd, false))\n}\n\nfunc (c *Cow) keyboard(evt *tcell.EventKey) *tcell.EventKey {\n\tif a, ok := c.actions.Get(ui.AsKey(evt)); ok {\n\t\treturn a.Action(evt)\n\t}\n\n\treturn evt\n}\n\n// StylesChanged notifies the skin changes.\nfunc (c *Cow) StylesChanged(s *config.Styles) {\n\tc.SetBackgroundColor(s.BgColor())\n\tc.SetTextColor(s.FgColor())\n\tc.SetBorderFocusColor(s.Frame().Border.FocusColor.Color())\n}\n\nfunc (c *Cow) resetCmd(evt *tcell.EventKey) *tcell.EventKey {\n\treturn c.app.PrevCmd(evt)\n}\n\n// Actions returns menu actions.\nfunc (c *Cow) Actions() *ui.KeyActions {\n\treturn c.actions\n}\n\n// Name returns the component name.\nfunc (*Cow) Name() string { return \"cow\" }\n\n// Start starts the view updater.\nfunc (*Cow) Start() {}\n\n// Stop terminates the updater.\nfunc (c *Cow) Stop() {\n\tc.app.Styles.RemoveListener(c)\n}\n\n// Hints returns menu hints.\nfunc (c *Cow) Hints() model.MenuHints {\n\treturn c.actions.Hints()\n}\n\n// ExtraHints returns additional hints.\nfunc (*Cow) ExtraHints() map[string]string {\n\treturn nil\n}\n\nfunc (c *Cow) updateTitle() {\n\tc.SetTitle(\" Error \")\n}\n\nvar cow = []string{\n\t`\\   ^__^            `,\n\t` \\  (oo)\\_______    `,\n\t`    (__)\\       )\\/\\`,\n\t`        ||----w |   `,\n\t`        ||     ||   `,\n}\n"
  },
  {
    "path": "internal/view/crd.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/ui\"\n)\n\n// CRD represents a crd viewer.\ntype CRD struct {\n\tResourceViewer\n}\n\n// NewCRD returns a new viewer.\nfunc NewCRD(gvr *client.GVR) ResourceViewer {\n\ts := CRD{\n\t\tResourceViewer: NewOwnerExtender(NewBrowser(gvr)),\n\t}\n\ts.GetTable().SetEnterFn(s.showCRD)\n\n\treturn &s\n}\n\nfunc (*CRD) showCRD(app *App, _ ui.Tabular, _ *client.GVR, path string) {\n\t_, crd := client.Namespaced(path)\n\tapp.gotoResource(crd, \"\", false, true)\n}\n"
  },
  {
    "path": "internal/view/cronjob.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/ui/dialog\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\tbatchv1 \"k8s.io/api/batch/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nconst (\n\tsuspendDialogKey     = \"suspend\"\n\tlastScheduledCol     = \"LAST_SCHEDULE\"\n\tdefaultSuspendStatus = \"true\"\n)\n\n// CronJob represents a cronjob viewer.\ntype CronJob struct {\n\tResourceViewer\n}\n\n// NewCronJob returns a new viewer.\nfunc NewCronJob(gvr *client.GVR) ResourceViewer {\n\tc := CronJob{ResourceViewer: NewVulnerabilityExtender(\n\t\tNewOwnerExtender(NewBrowser(gvr)),\n\t)}\n\tc.AddBindKeysFn(c.bindKeys)\n\tc.GetTable().SetEnterFn(c.showJobs)\n\n\treturn &c\n}\n\nfunc (*CronJob) showJobs(app *App, _ ui.Tabular, gvr *client.GVR, fqn string) {\n\tslog.Debug(\"Showing Jobs\", slogs.GVR, gvr, slogs.FQN, fqn)\n\to, err := app.factory.Get(gvr, fqn, true, labels.Everything())\n\tif err != nil {\n\t\tapp.Flash().Err(err)\n\t\treturn\n\t}\n\n\tvar cj batchv1.CronJob\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cj)\n\tif err != nil {\n\t\tapp.Flash().Err(err)\n\t\treturn\n\t}\n\n\tns, _ := client.Namespaced(fqn)\n\tif err := app.Config.SetActiveNamespace(ns); err != nil {\n\t\tslog.Error(\"Unable to set active namespace during show pods\", slogs.Error, err)\n\t}\n\tv := NewJob(client.JobGVR)\n\tv.SetContextFn(jobCtx(fqn, string(cj.UID)))\n\tif err := app.inject(v, false); err != nil {\n\t\tapp.Flash().Err(err)\n\t}\n}\n\nfunc jobCtx(fqn, uid string) ContextFunc {\n\treturn func(ctx context.Context) context.Context {\n\t\tctx = context.WithValue(ctx, internal.KeyPath, fqn)\n\t\treturn context.WithValue(ctx, internal.KeyUID, uid)\n\t}\n}\n\nfunc (c *CronJob) bindKeys(aa *ui.KeyActions) {\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyT: ui.NewKeyAction(\"Trigger\", c.triggerCmd, true),\n\t\tui.KeyS: ui.NewKeyAction(\"Suspend/Resume\", c.toggleSuspendCmd, true),\n\t})\n}\n\nfunc (c *CronJob) triggerCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tfqns := c.GetTable().GetSelectedItems()\n\tif len(fqns) == 0 {\n\t\treturn evt\n\t}\n\tmsg := fmt.Sprintf(\"Trigger CronJob: %s?\", fqns[0])\n\tif len(fqns) > 1 {\n\t\tmsg = fmt.Sprintf(\"Trigger %d CronJobs?\", len(fqns))\n\t}\n\td := c.App().Styles.Dialog()\n\tdialog.ShowConfirm(&d, c.App().Content.Pages, \"Confirm Job Trigger\", msg, func() {\n\t\tres, err := dao.AccessorFor(c.App().factory, c.GVR())\n\t\tif err != nil {\n\t\t\tc.App().Flash().Err(fmt.Errorf(\"no accessor for %q\", c.GVR()))\n\t\t\treturn\n\t\t}\n\t\trunner, ok := res.(dao.Runnable)\n\t\tif !ok {\n\t\t\tc.App().Flash().Err(fmt.Errorf(\"expecting a job runner resource for %q\", c.GVR()))\n\t\t\treturn\n\t\t}\n\n\t\tfor _, fqn := range fqns {\n\t\t\tif err := runner.Run(fqn); err != nil {\n\t\t\t\tc.App().Flash().Errf(\"CronJob trigger failed for %s: %v\", fqn, err)\n\t\t\t} else {\n\t\t\t\tc.App().Flash().Infof(\"Triggered Job %s %s\", c.GVR(), fqn)\n\t\t\t}\n\t\t}\n\t}, func() {})\n\n\treturn nil\n}\n\nfunc (c *CronJob) toggleSuspendCmd(evt *tcell.EventKey) *tcell.EventKey {\n\ttable := c.GetTable()\n\tsel := table.GetSelectedItem()\n\n\tif sel == \"\" {\n\t\treturn evt\n\t}\n\n\tcell := table.GetCell(c.GetTable().GetSelectedRowIndex(), c.GetTable().NameColIndex()+2)\n\n\tif cell == nil {\n\t\tc.App().Flash().Errf(\"Unable to assert current status\")\n\t\treturn nil\n\t}\n\n\tc.Stop()\n\tdefer c.Start()\n\n\tc.showSuspendDialog(cell, sel)\n\n\treturn nil\n}\n\nfunc (c *CronJob) showSuspendDialog(cell *tview.TableCell, sel string) {\n\ttitle := \"Suspend\"\n\n\tif strings.TrimSpace(cell.Text) == defaultSuspendStatus {\n\t\ttitle = \"Resume\"\n\t}\n\n\td := c.App().Styles.Dialog()\n\tdialog.ShowConfirm(&d, c.App().Content.Pages, title, sel, func() {\n\t\tctx, cancel := context.WithTimeout(context.Background(), c.App().Conn().Config().CallTimeout())\n\t\tdefer cancel()\n\n\t\tres, err := dao.AccessorFor(c.App().factory, c.GVR())\n\t\tif err != nil {\n\t\t\tc.App().Flash().Err(fmt.Errorf(\"no accessor for %q\", c.GVR()))\n\t\t\treturn\n\t\t}\n\n\t\tcronJob, ok := res.(*dao.CronJob)\n\t\tif !ok {\n\t\t\tc.App().Flash().Errf(\"expecting a cron job for %q\", c.GVR())\n\t\t\treturn\n\t\t}\n\n\t\tif err := cronJob.ToggleSuspend(ctx, sel); err != nil {\n\t\t\tc.App().Flash().Errf(\"Cronjob %s failed for %v\", strings.ToLower(title), err)\n\t\t\treturn\n\t\t}\n\t}, func() {})\n}\n"
  },
  {
    "path": "internal/view/details.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\t\"github.com/sahilm/fuzzy\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n)\n\nconst (\n\tdetailsTitleFmt = \"[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] \"\n\tcontentTXT      = \"text\"\n\tcontentYAML     = \"yaml\"\n)\n\n// Details represents a generic text viewer.\ntype Details struct {\n\t*tview.Flex\n\n\ttext                      *tview.TextView\n\tactions                   *ui.KeyActions\n\tapp                       *App\n\ttitle, subject            string\n\tcmdBuff                   *model.FishBuff\n\tmodel                     *model.Text\n\tcurrentRegion, maxRegions int\n\tsearchable                bool\n\tfullScreen                bool\n\tcontentType               string\n}\n\n// NewDetails returns a details viewer.\nfunc NewDetails(app *App, title, subject, contentType string, searchable bool) *Details {\n\td := Details{\n\t\tFlex:        tview.NewFlex(),\n\t\ttext:        tview.NewTextView(),\n\t\tapp:         app,\n\t\ttitle:       title,\n\t\tsubject:     subject,\n\t\tactions:     ui.NewKeyActions(),\n\t\tcmdBuff:     model.NewFishBuff('/', model.FilterBuffer),\n\t\tmodel:       model.NewText(),\n\t\tsearchable:  searchable,\n\t\tcontentType: contentType,\n\t}\n\td.AddItem(d.text, 0, 1, true)\n\n\treturn &d\n}\n\nfunc (*Details) SetCommand(*cmd.Interpreter)            {}\nfunc (*Details) SetFilter(string, bool)                 {}\nfunc (*Details) SetLabelSelector(labels.Selector, bool) {}\n\n// Init initializes the viewer.\nfunc (d *Details) Init(_ context.Context) error {\n\tif d.title != \"\" {\n\t\td.SetBorder(true)\n\t}\n\td.text.SetScrollable(true).SetWrap(true).SetRegions(true)\n\td.text.SetDynamicColors(true)\n\td.text.SetHighlightColor(tcell.ColorOrange)\n\td.SetTitleColor(tcell.ColorAqua)\n\td.SetInputCapture(d.keyboard)\n\td.SetBorderPadding(0, 0, 1, 1)\n\td.updateTitle()\n\n\td.app.Styles.AddListener(d)\n\td.StylesChanged(d.app.Styles)\n\td.setFullScreen(d.app.Config.K9s.UI.DefaultsToFullScreen)\n\n\td.app.Prompt().SetModel(d.cmdBuff)\n\td.cmdBuff.AddListener(d)\n\n\td.bindKeys()\n\td.SetInputCapture(d.keyboard)\n\td.model.AddListener(d)\n\n\treturn nil\n}\n\n// InCmdMode checks if prompt is active.\nfunc (d *Details) InCmdMode() bool {\n\treturn d.cmdBuff.InCmdMode()\n}\n\n// TextChanged notifies the model changed.\nfunc (d *Details) TextChanged(lines []string) {\n\tswitch d.contentType {\n\tcase contentYAML:\n\t\td.text.SetText(colorizeYAML(d.app.Styles.Views().Yaml, strings.Join(lines, \"\\n\")))\n\tdefault:\n\t\td.text.SetText(strings.Join(lines, \"\\n\"))\n\t}\n\td.text.ScrollToBeginning()\n}\n\n// TextFiltered notifies when the filter changed.\nfunc (d *Details) TextFiltered(lines []string, matches fuzzy.Matches) {\n\td.currentRegion, d.maxRegions = 0, len(matches)\n\tll := linesWithRegions(lines, matches)\n\n\td.text.SetText(colorizeYAML(d.app.Styles.Views().Yaml, strings.Join(ll, \"\\n\")))\n\td.text.Highlight()\n\tif len(matches) > 0 {\n\t\td.text.Highlight(\"search_0\")\n\t\td.text.ScrollToHighlight()\n\t}\n}\n\n// BufferChanged indicates the buffer was changed.\nfunc (*Details) BufferChanged(_, _ string) {}\n\n// BufferCompleted indicates input was accepted.\nfunc (d *Details) BufferCompleted(text, _ string) {\n\td.model.Filter(text)\n\td.updateTitle()\n}\n\n// BufferActive indicates the buff activity changed.\nfunc (d *Details) BufferActive(state bool, k model.BufferKind) {\n\td.app.BufferActive(state, k)\n}\n\nfunc (d *Details) bindKeys() {\n\td.actions.Bulk(ui.KeyMap{\n\t\ttcell.KeyEnter:  ui.NewSharedKeyAction(\"Filter\", d.filterCmd, false),\n\t\ttcell.KeyEscape: ui.NewKeyAction(\"Back\", d.resetCmd, false),\n\t\tui.KeyQ:         ui.NewKeyAction(\"Back\", d.resetCmd, false),\n\t\ttcell.KeyCtrlS:  ui.NewKeyAction(\"Save\", d.saveCmd, false),\n\t\tui.KeyC:         ui.NewKeyAction(\"Copy\", cpCmd(d.app.Flash(), d.text), true),\n\t\tui.KeyF:         ui.NewKeyAction(\"Toggle FullScreen\", d.toggleFullScreenCmd, true),\n\t\tui.KeyN:         ui.NewKeyAction(\"Next Match\", d.nextCmd, true),\n\t\tui.KeyShiftN:    ui.NewKeyAction(\"Prev Match\", d.prevCmd, true),\n\t\tui.KeySlash:     ui.NewSharedKeyAction(\"Filter Mode\", d.activateCmd, false),\n\t\ttcell.KeyDelete: ui.NewSharedKeyAction(\"Erase\", d.eraseCmd, false),\n\t})\n\n\tif !d.searchable {\n\t\td.actions.Delete(ui.KeyN, ui.KeyShiftN)\n\t}\n}\n\nfunc (d *Details) keyboard(evt *tcell.EventKey) *tcell.EventKey {\n\tif a, ok := d.actions.Get(ui.AsKey(evt)); ok {\n\t\treturn a.Action(evt)\n\t}\n\n\treturn evt\n}\n\n// StylesChanged notifies the skin changed.\nfunc (d *Details) StylesChanged(s *config.Styles) {\n\td.SetBackgroundColor(s.BgColor())\n\td.text.SetTextColor(s.FgColor())\n\td.SetBorderFocusColor(s.Frame().Border.FocusColor.Color())\n\td.TextChanged(d.model.Peek())\n}\n\n// Update updates the view content.\nfunc (d *Details) Update(buff string) *Details {\n\td.model.SetText(buff)\n\n\treturn d\n}\n\nfunc (d *Details) GetWriter() io.Writer {\n\treturn d.text\n}\n\n// SetSubject updates the subject.\nfunc (d *Details) SetSubject(s string) {\n\td.subject = s\n}\n\n// Actions returns menu actions.\nfunc (d *Details) Actions() *ui.KeyActions {\n\treturn d.actions\n}\n\n// Name returns the component name.\nfunc (d *Details) Name() string { return d.title }\n\n// Start starts the view updater.\nfunc (*Details) Start() {}\n\n// Stop terminates the updater.\nfunc (d *Details) Stop() {\n\td.app.Styles.RemoveListener(d)\n}\n\n// Hints returns menu hints.\nfunc (d *Details) Hints() model.MenuHints {\n\treturn d.actions.Hints()\n}\n\n// ExtraHints returns additional hints.\nfunc (*Details) ExtraHints() map[string]string {\n\treturn nil\n}\n\nfunc (d *Details) nextCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif d.cmdBuff.Empty() {\n\t\treturn evt\n\t}\n\n\td.currentRegion++\n\tif d.currentRegion >= d.maxRegions {\n\t\td.currentRegion = 0\n\t}\n\td.text.Highlight(fmt.Sprintf(\"search_%d\", d.currentRegion))\n\td.text.ScrollToHighlight()\n\td.updateTitle()\n\n\treturn nil\n}\n\nfunc (d *Details) toggleFullScreenCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif d.app.InCmdMode() {\n\t\treturn evt\n\t}\n\n\td.setFullScreen(!d.fullScreen)\n\n\treturn nil\n}\n\nfunc (d *Details) setFullScreen(isFullScreen bool) {\n\td.fullScreen = isFullScreen\n\td.SetFullScreen(isFullScreen)\n\td.SetBorder(!isFullScreen)\n\tif isFullScreen {\n\t\td.SetBorderPadding(0, 0, 0, 0)\n\t} else {\n\t\td.SetBorderPadding(0, 0, 1, 1)\n\t}\n}\n\nfunc (d *Details) prevCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif d.cmdBuff.Empty() {\n\t\treturn evt\n\t}\n\n\td.currentRegion--\n\tif d.currentRegion < 0 {\n\t\td.currentRegion = d.maxRegions - 1\n\t}\n\td.text.Highlight(fmt.Sprintf(\"search_%d\", d.currentRegion))\n\td.text.ScrollToHighlight()\n\td.updateTitle()\n\n\treturn nil\n}\n\nfunc (d *Details) filterCmd(*tcell.EventKey) *tcell.EventKey {\n\td.model.Filter(d.cmdBuff.GetText())\n\td.cmdBuff.SetActive(false)\n\td.updateTitle()\n\n\treturn nil\n}\n\nfunc (d *Details) activateCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif d.app.InCmdMode() {\n\t\treturn evt\n\t}\n\td.app.ResetPrompt(d.cmdBuff)\n\n\treturn nil\n}\n\nfunc (d *Details) eraseCmd(*tcell.EventKey) *tcell.EventKey {\n\tif !d.cmdBuff.IsActive() {\n\t\treturn nil\n\t}\n\td.cmdBuff.Delete()\n\n\treturn nil\n}\n\nfunc (d *Details) resetCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif !d.cmdBuff.InCmdMode() {\n\t\td.cmdBuff.Reset()\n\t\treturn d.app.PrevCmd(evt)\n\t}\n\n\tif d.cmdBuff.GetText() != \"\" {\n\t\td.model.ClearFilter()\n\t}\n\td.cmdBuff.SetActive(false)\n\td.cmdBuff.Reset()\n\td.updateTitle()\n\n\treturn nil\n}\n\nfunc (d *Details) saveCmd(*tcell.EventKey) *tcell.EventKey {\n\tif path, err := saveYAML(d.app.Config.K9s.ContextScreenDumpDir(), d.title, d.text.GetText(true)); err != nil {\n\t\td.app.Flash().Err(err)\n\t} else {\n\t\td.app.Flash().Infof(\"Log %s saved successfully!\", path)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Details) updateTitle() {\n\tif d.title == \"\" {\n\t\treturn\n\t}\n\tfmat := fmt.Sprintf(detailsTitleFmt, d.title, d.subject)\n\n\tvar (\n\t\tbuff   = d.cmdBuff.GetText()\n\t\tstyles = d.app.Styles.Frame()\n\t)\n\tif buff == \"\" {\n\t\td.SetTitle(ui.SkinTitle(fmat, &styles))\n\t\treturn\n\t}\n\n\tif d.maxRegions != 0 {\n\t\tbuff += fmt.Sprintf(\"[%d:%d]\", d.currentRegion+1, d.maxRegions)\n\t}\n\tfmat += fmt.Sprintf(ui.SearchFmt, buff)\n\td.SetTitle(ui.SkinTitle(fmat, &styles))\n}\n"
  },
  {
    "path": "internal/view/dir.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path\"\n\t\"slices\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/ui/dialog\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\nconst (\n\tkustomize      = \"kustomization\"\n\tkustomizeNoExt = \"Kustomization\"\n\tkustomizeYAML  = kustomize + extYAML\n\tkustomizeYML   = kustomize + extYML\n\textYAML        = \".yaml\"\n\textYML         = \".yml\"\n)\n\n// Dir represents a command directory view.\ntype Dir struct {\n\tResourceViewer\n\tpath string\n}\n\n// NewDir returns a new instance.\nfunc NewDir(s string) ResourceViewer {\n\td := Dir{\n\t\tResourceViewer: NewBrowser(client.DirGVR),\n\t\tpath:           s,\n\t}\n\td.GetTable().SetBorderFocusColor(tcell.ColorAliceBlue)\n\td.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorAliceBlue).Attributes(tcell.AttrNone))\n\td.AddBindKeysFn(d.bindKeys)\n\td.SetContextFn(d.dirContext)\n\n\treturn &d\n}\n\n// Init initializes the view.\nfunc (d *Dir) Init(ctx context.Context) error {\n\tif err := d.ResourceViewer.Init(ctx); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (d *Dir) dirContext(ctx context.Context) context.Context {\n\treturn context.WithValue(ctx, internal.KeyPath, d.path)\n}\n\nfunc (d *Dir) bindDangerousKeys(aa *ui.KeyActions) {\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyA: ui.NewKeyActionWithOpts(\"Apply\", d.applyCmd, ui.ActionOpts{\n\t\t\tVisible:   true,\n\t\t\tDangerous: true,\n\t\t}),\n\t\tui.KeyD: ui.NewKeyActionWithOpts(\"Delete\", d.delCmd, ui.ActionOpts{\n\t\t\tVisible:   true,\n\t\t\tDangerous: true,\n\t\t}),\n\t\tui.KeyE: ui.NewKeyActionWithOpts(\"Edit\", d.editCmd, ui.ActionOpts{\n\t\t\tVisible:   true,\n\t\t\tDangerous: true,\n\t\t}),\n\t})\n}\n\nfunc (d *Dir) bindKeys(aa *ui.KeyActions) {\n\t// !!BOZO!! Lame!\n\taa.Delete(ui.KeyShiftA, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace)\n\taa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL, tcell.KeyCtrlD, tcell.KeyCtrlZ)\n\tif !d.App().Config.IsReadOnly() {\n\t\td.bindDangerousKeys(aa)\n\t}\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyY:        ui.NewKeyAction(yamlAction, d.viewCmd, true),\n\t\ttcell.KeyEnter: ui.NewKeyAction(\"Goto\", d.gotoCmd, true),\n\t})\n}\n\nfunc (d *Dir) viewCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tsel := d.GetTable().GetSelectedItem()\n\tif sel == \"\" {\n\t\treturn evt\n\t}\n\n\tif path.Ext(sel) == \"\" {\n\t\treturn nil\n\t}\n\n\tyaml, err := os.ReadFile(sel)\n\tif err != nil {\n\t\td.App().Flash().Err(err)\n\t\treturn nil\n\t}\n\n\tdetails := NewDetails(d.App(), yamlAction, sel, contentYAML, true).Update(string(yaml))\n\tif err := d.App().inject(details, false); err != nil {\n\t\td.App().Flash().Err(err)\n\t}\n\n\treturn nil\n}\n\nfunc isManifest(s string) bool {\n\text := path.Ext(s)\n\treturn ext == \".yml\" || ext == \".yaml\"\n}\n\nfunc (d *Dir) editCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tsel := d.GetTable().GetSelectedItem()\n\tif sel == \"\" {\n\t\treturn evt\n\t}\n\n\tif !isManifest(sel) {\n\t\td.App().Flash().Errf(\"you must select a manifest\")\n\t\treturn nil\n\t}\n\n\td.Stop()\n\tdefer d.Start()\n\tif !edit(d.App(), &shellOpts{clear: true, args: []string{sel}}) {\n\t\td.App().Flash().Errf(\"Failed to launch editor\")\n\t}\n\n\treturn nil\n}\n\nfunc (d *Dir) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif d.GetTable().CmdBuff().IsActive() {\n\t\treturn d.GetTable().activateCmd(evt)\n\t}\n\n\tsel := d.GetTable().GetSelectedItem()\n\tif sel == \"\" {\n\t\treturn evt\n\t}\n\n\tif isManifest(sel) {\n\t\td.App().Flash().Errf(\"you must select a directory\")\n\t\treturn nil\n\t}\n\n\tv := NewDir(sel)\n\tif err := d.App().inject(v, false); err != nil {\n\t\td.App().Flash().Err(err)\n\t}\n\n\treturn evt\n}\n\nfunc isKustomized(sel string) bool {\n\tif isManifest(sel) {\n\t\treturn false\n\t}\n\n\tff, err := os.ReadDir(sel)\n\tif err != nil {\n\t\treturn false\n\t}\n\tkk := []string{kustomizeNoExt, kustomizeYAML, kustomizeYML}\n\tfor _, f := range ff {\n\t\tif slices.Contains(kk, f.Name()) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc containsDir(sel string) bool {\n\tif isManifest(sel) {\n\t\treturn false\n\t}\n\n\tff, err := os.ReadDir(sel)\n\tif err != nil {\n\t\treturn false\n\t}\n\tfor _, f := range ff {\n\t\tif f.IsDir() {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (d *Dir) applyCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tsel := d.GetTable().GetSelectedItem()\n\tif sel == \"\" {\n\t\treturn evt\n\t}\n\n\topts := []string{\"-f\"}\n\tif containsDir(sel) {\n\t\topts = append(opts, \"-R\")\n\t}\n\tif isKustomized(sel) {\n\t\topts = []string{\"-k\"}\n\t}\n\td.Stop()\n\tdefer d.Start()\n\t{\n\t\targs := make([]string, 0, 10)\n\t\targs = append(args, \"apply\")\n\t\targs = append(args, opts...)\n\t\targs = append(args, sel)\n\t\tres, err := runKu(context.Background(), d.App(), &shellOpts{clear: false, args: args})\n\t\tif err != nil {\n\t\t\tres = \"status:\\n  \" + err.Error() + \"\\nmessage:\\n\" + fmtResults(res)\n\t\t} else {\n\t\t\tres = \"message:\\n\" + fmtResults(res)\n\t\t}\n\n\t\tdetails := NewDetails(d.App(), \"Applied Manifest\", sel, contentYAML, true).Update(res)\n\t\tif err := d.App().inject(details, false); err != nil {\n\t\t\td.App().Flash().Err(err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (d *Dir) delCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tsel := d.GetTable().GetSelectedItem()\n\tif sel == \"\" {\n\t\treturn evt\n\t}\n\n\topts := []string{\"-f\"}\n\tmsgResource := \"manifest\"\n\tif containsDir(sel) {\n\t\topts = append(opts, \"-R\")\n\t}\n\tif isKustomized(sel) {\n\t\topts = []string{\"-k\"}\n\t\tmsgResource = \"kustomization\"\n\t}\n\n\td.Stop()\n\tdefer d.Start()\n\tmsg := fmt.Sprintf(\"Delete resource(s) in %s %s\", msgResource, sel)\n\tdlg := d.App().Styles.Dialog()\n\tdialog.ShowConfirm(&dlg, d.App().Content.Pages, \"Confirm Delete\", msg, func() {\n\t\targs := make([]string, 0, 10)\n\t\targs = append(args, \"delete\")\n\t\targs = append(args, opts...)\n\t\targs = append(args, sel)\n\t\tres, err := runKu(context.Background(), d.App(), &shellOpts{clear: false, args: args})\n\t\tif err != nil {\n\t\t\tres = \"status:\\n  \" + err.Error() + \"\\nmessage:\\n\" + fmtResults(res)\n\t\t} else {\n\t\t\tres = \"message:\\n\" + fmtResults(res)\n\t\t}\n\t\tdetails := NewDetails(d.App(), \"Deleted Manifest\", sel, contentYAML, true).Update(res)\n\t\tif err := d.App().inject(details, false); err != nil {\n\t\t\td.App().Flash().Err(err)\n\t\t}\n\t}, func() {})\n\n\treturn nil\n}\n\nfunc fmtResults(res string) string {\n\tres = strings.TrimSpace(res)\n\tlines := strings.Split(res, \"\\n\")\n\tll := make([]string, 0, len(lines))\n\tfor _, l := range lines {\n\t\tll = append(ll, \"  \"+l)\n\t}\n\treturn strings.Join(ll, \"\\n\")\n}\n"
  },
  {
    "path": "internal/view/dir_int_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestIsManifest(t *testing.T) {\n\tuu := map[string]struct {\n\t\tfile string\n\t\te    bool\n\t}{\n\t\t\"yaml\": {file: \"fred.yaml\", e: true},\n\t\t\"yml\":  {file: \"fred.yml\", e: true},\n\t\t\"nope\": {file: \"fred.txt\"},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, isManifest(u.file))\n\t\t})\n\t}\n}\n\nfunc TestIsKustomized(t *testing.T) {\n\tuu := map[string]struct {\n\t\tpath string\n\t\te    bool\n\t}{\n\t\t\"toast\": {path: \"testdata/fred\"},\n\t\t\"yaml\":  {path: \"testdata/kmanifests\", e: true},\n\t\t\"yml\":   {path: \"testdata/k1manifests\", e: true},\n\t\t\"noExt\": {path: \"testdata/k2manifests\", e: true},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, isKustomized(u.path))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/view/dir_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDir(t *testing.T) {\n\tv := view.NewDir(\"/fred\")\n\n\trequire.NoError(t, v.Init(makeCtx(t)))\n\tassert.Equal(t, \"Directory\", v.Name())\n\tassert.Len(t, v.Hints(), 9)\n}\n"
  },
  {
    "path": "internal/view/dp.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"errors\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nconst scaleDialogKey = \"scale\"\n\n// Deploy represents a deployment view.\ntype Deploy struct {\n\tResourceViewer\n}\n\n// NewDeploy returns a new deployment view.\nfunc NewDeploy(gvr *client.GVR) ResourceViewer {\n\tvar d Deploy\n\td.ResourceViewer = NewPortForwardExtender(\n\t\tNewVulnerabilityExtender(\n\t\t\tNewRestartExtender(\n\t\t\t\tNewScaleExtender(\n\t\t\t\t\tNewImageExtender(\n\t\t\t\t\t\tNewOwnerExtender(\n\t\t\t\t\t\t\tNewLogsExtender(NewBrowser(gvr), d.logOptions),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t)\n\td.AddBindKeysFn(d.bindKeys)\n\td.GetTable().SetEnterFn(d.showPods)\n\n\treturn &d\n}\n\nfunc (d *Deploy) bindKeys(aa *ui.KeyActions) {\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyZ: ui.NewKeyAction(\"ReplicaSets\", d.replicaSetsCmd, true),\n\t})\n}\n\nfunc (d *Deploy) logOptions(prev bool) (*dao.LogOptions, error) {\n\tpath := d.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn nil, errors.New(\"you must provide a selection\")\n\t}\n\tdp, err := d.getInstance(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn podLogOptions(d.App(), path, prev, &dp.ObjectMeta, &dp.Spec.Template.Spec), nil\n}\n\nfunc (d *Deploy) replicaSetsCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tdName := d.GetTable().GetSelectedItem()\n\tif dName == \"\" {\n\t\treturn evt\n\t}\n\tdp, err := d.getInstance(dName)\n\tif err != nil {\n\t\td.App().Flash().Err(err)\n\t\treturn nil\n\t}\n\tshowReplicasetsFromSelector(d.App(), dName, dp.Spec.Selector)\n\treturn nil\n}\n\nfunc (d *Deploy) showPods(app *App, _ ui.Tabular, _ *client.GVR, fqn string) {\n\tdp, err := d.getInstance(fqn)\n\tif err != nil {\n\t\tapp.Flash().Err(err)\n\t\treturn\n\t}\n\n\tshowPodsFromSelector(app, fqn, dp.Spec.Selector)\n}\n\nfunc (d *Deploy) getInstance(fqn string) (*appsv1.Deployment, error) {\n\tvar dp dao.Deployment\n\tdp.Init(d.App().factory, d.GVR())\n\n\treturn dp.GetInstance(fqn)\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc showPodsFromSelector(app *App, path string, sel *metav1.LabelSelector) {\n\tl, err := metav1.LabelSelectorAsSelector(sel)\n\tif err != nil {\n\t\tapp.Flash().Err(err)\n\t\treturn\n\t}\n\n\tshowPods(app, path, l, \"\")\n}\n\nfunc showReplicasetsFromSelector(app *App, path string, sel *metav1.LabelSelector) {\n\tl, err := metav1.LabelSelectorAsSelector(sel)\n\tif err != nil {\n\t\tapp.Flash().Err(err)\n\t\treturn\n\t}\n\n\tshowReplicasets(app, path, l, \"\")\n}\n"
  },
  {
    "path": "internal/view/dp_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDeploy(t *testing.T) {\n\tv := view.NewDeploy(client.DpGVR)\n\n\trequire.NoError(t, v.Init(makeCtx(t)))\n\tassert.Equal(t, \"Deployments\", v.Name())\n\tassert.Len(t, v.Hints(), 15)\n}\n"
  },
  {
    "path": "internal/view/drain_dialog.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tview\"\n)\n\nconst drainKey = \"drain\"\n\n// DrainFunc represents a drain callback function.\ntype DrainFunc func(v ResourceViewer, sels []string, opts dao.DrainOptions)\n\n// ShowDrain pops a node drain dialog.\nfunc ShowDrain(view ResourceViewer, sels []string, opts dao.DrainOptions, okFn DrainFunc) {\n\tstyles := view.App().Styles.Dialog()\n\n\tf := tview.NewForm().\n\t\tSetItemPadding(0).\n\t\tSetButtonsAlign(tview.AlignCenter).\n\t\tSetButtonBackgroundColor(styles.ButtonBgColor.Color()).\n\t\tSetButtonTextColor(styles.ButtonFgColor.Color()).\n\t\tSetLabelColor(styles.LabelFgColor.Color()).\n\t\tSetFieldTextColor(styles.FieldFgColor.Color()).\n\t\tSetFieldBackgroundColor(styles.BgColor.Color())\n\n\tf.AddInputField(\"GracePeriod:\", strconv.Itoa(opts.GracePeriodSeconds), 0, nil, func(v string) {\n\t\ta, err := asIntOpt(v)\n\t\tif err != nil {\n\t\t\tview.App().Flash().Err(err)\n\t\t\treturn\n\t\t}\n\t\tview.App().Flash().Clear()\n\t\topts.GracePeriodSeconds = a\n\t})\n\tf.AddInputField(\"Timeout:\", opts.Timeout.String(), 0, nil, func(v string) {\n\t\ta, err := asDurOpt(v)\n\t\tif err != nil {\n\t\t\tview.App().Flash().Err(err)\n\t\t\treturn\n\t\t}\n\t\tview.App().Flash().Clear()\n\t\topts.Timeout = a\n\t})\n\tf.AddCheckbox(\"Ignore DaemonSets:\", opts.IgnoreAllDaemonSets, func(_ string, v bool) {\n\t\topts.IgnoreAllDaemonSets = v\n\t})\n\tf.AddCheckbox(\"Delete EmptyDir Data:\", opts.DeleteEmptyDirData, func(_ string, v bool) {\n\t\topts.DeleteEmptyDirData = v\n\t})\n\tf.AddCheckbox(\"Force:\", opts.Force, func(_ string, v bool) {\n\t\topts.Force = v\n\t})\n\tf.AddCheckbox(\"Disable Eviction:\", opts.DisableEviction, func(_ string, v bool) {\n\t\topts.DisableEviction = v\n\t})\n\n\tpages := view.App().Content.Pages\n\tf.AddButton(\"Cancel\", func() {\n\t\tDismissDrain(view, pages)\n\t})\n\tf.AddButton(\"OK\", func() {\n\t\tDismissDrain(view, pages)\n\t\tokFn(view, sels, opts)\n\t})\n\n\tmodal := tview.NewModalForm(\"<Drain>\", f)\n\tpath := \"Drain \"\n\tif len(sels) == 1 {\n\t\tpath += sels[0]\n\t} else {\n\t\tpath += fmt.Sprintf(\"(%d) nodes\", len(sels))\n\t}\n\tpath += \"?\"\n\tmodal.SetText(path)\n\tmodal.SetDoneFunc(func(int, string) {\n\t\tDismissDrain(view, pages)\n\t})\n\n\tpages.AddPage(drainKey, modal, false, true)\n\tpages.ShowPage(drainKey)\n\tview.App().SetFocus(pages.GetPrimitive(drainKey))\n}\n\n// DismissDrain dismiss the port forward dialog.\nfunc DismissDrain(v ResourceViewer, p *ui.Pages) {\n\tp.RemovePage(drainKey)\n\tv.App().SetFocus(p.CurrentPage().Item)\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc asDurOpt(v string) (time.Duration, error) {\n\td, err := time.ParseDuration(v)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn d, nil\n}\n\nfunc asIntOpt(v string) (int, error) {\n\ti, err := strconv.Atoi(v)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn i, nil\n}\n"
  },
  {
    "path": "internal/view/ds.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"errors\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n)\n\n// DaemonSet represents a daemon set custom viewer.\ntype DaemonSet struct {\n\tResourceViewer\n}\n\n// NewDaemonSet returns a new viewer.\nfunc NewDaemonSet(gvr *client.GVR) ResourceViewer {\n\tvar d DaemonSet\n\td.ResourceViewer = NewPortForwardExtender(\n\t\tNewVulnerabilityExtender(\n\t\t\tNewRestartExtender(\n\t\t\t\tNewImageExtender(\n\t\t\t\t\tNewOwnerExtender(\n\t\t\t\t\t\tNewLogsExtender(NewBrowser(gvr), d.logOptions),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t)\n\td.GetTable().SetEnterFn(d.showPods)\n\n\treturn &d\n}\n\nfunc (d *DaemonSet) showPods(app *App, _ ui.Tabular, _ *client.GVR, path string) {\n\tvar res dao.DaemonSet\n\tres.Init(app.factory, d.GVR())\n\n\tds, err := res.GetInstance(path)\n\tif err != nil {\n\t\td.App().Flash().Err(err)\n\t\treturn\n\t}\n\n\tshowPodsFromSelector(app, path, ds.Spec.Selector)\n}\n\nfunc (d *DaemonSet) logOptions(prev bool) (*dao.LogOptions, error) {\n\tpath := d.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn nil, errors.New(\"you must provide a selection\")\n\t}\n\tds, err := d.getInstance(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn podLogOptions(d.App(), path, prev, &ds.ObjectMeta, &ds.Spec.Template.Spec), nil\n}\n\nfunc (d *DaemonSet) getInstance(fqn string) (*appsv1.DaemonSet, error) {\n\tvar ds dao.DaemonSet\n\tds.Init(d.App().factory, client.DsGVR)\n\n\treturn ds.GetInstance(fqn)\n}\n"
  },
  {
    "path": "internal/view/ds_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDaemonSet(t *testing.T) {\n\tv := view.NewDaemonSet(client.DsGVR)\n\n\trequire.NoError(t, v.Init(makeCtx(t)))\n\tassert.Equal(t, \"DaemonSets\", v.Name())\n\tassert.Len(t, v.Hints(), 14)\n}\n"
  },
  {
    "path": "internal/view/env.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/slogs\"\n)\n\n// Env represent K9s and K8s available environment variables.\ntype Env map[string]string\n\n// EnvRX match $XXX, $!XXX, ${XXX} or ${!XXX} custom arg.\n// |\n// |                               (g2)(group 3)       (g5)( group 6   )\n// |                            (    group 1    ) (       group 4        )\n// |                           (                 group 0                  )\nvar envRX = regexp.MustCompile(`(\\$(!?)([\\w\\-]+))|(\\$\\{(!?)([\\w\\-%/: ]+)})`)\n\n// keyFromSubmatch extracts the name and inverse flag of a match.\nfunc keyFromSubmatch(m []string) (key string, inverse bool) {\n\t// group 1 matches $XXX and $!XXX args.\n\tif m[1] != \"\" {\n\t\treturn m[3], m[2] == \"!\"\n\t}\n\t// group 4 matches ${XXX} and ${!XXX} args.\n\treturn m[6], m[5] == \"!\"\n}\n\n// Substitute replaces env variable keys from in a string with their corresponding values.\nfunc (e Env) Substitute(arg string) (string, error) {\n\tmatches := envRX.FindAllStringSubmatch(arg, -1)\n\tif len(matches) == 0 {\n\t\treturn arg, nil\n\t}\n\n\t// To prevent the substitution starts with the shorter environment variable,\n\t// sort with the length of the found environment variables.\n\tsort.Slice(matches, func(i, j int) bool {\n\t\treturn len(matches[i][0]) > len(matches[j][0])\n\t})\n\n\tfor _, m := range matches {\n\t\tkey, inverse := keyFromSubmatch(m)\n\t\tv, ok := e[strings.ToUpper(key)]\n\t\tif !ok {\n\t\t\tslog.Warn(\"No k9s environment matching key\",\n\t\t\t\tslogs.Matches, matches,\n\t\t\t\tslogs.Key, key,\n\t\t\t)\n\t\t\tcontinue\n\t\t}\n\t\tif b, err := strconv.ParseBool(v); err == nil {\n\t\t\tif inverse {\n\t\t\t\tb = !b\n\t\t\t}\n\t\t\tv = fmt.Sprintf(\"%t\", b)\n\t\t}\n\t\targ = strings.ReplaceAll(arg, m[0], v)\n\t}\n\n\treturn arg, nil\n}\n"
  },
  {
    "path": "internal/view/env_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestEnvReplace(t *testing.T) {\n\tuu := map[string]struct {\n\t\targ string\n\t\terr error\n\t\te   string\n\t}{\n\t\t\"no-args\":   {arg: \"blee blah\", e: \"blee blah\"},\n\t\t\"simple\":    {arg: \"$A\", e: \"10\"},\n\t\t\"substring\": {arg: \"$A and $AA\", e: \"10 and 20\"},\n\t\t\"with-text\": {arg: \"Something $A\", e: \"Something 10\"},\n\t\t\"noMatch\":   {arg: \"blah blah and $BLEE\", e: \"blah blah and $BLEE\"},\n\t\t\"lower\":     {arg: \"And then $b happened\", e: \"And then blee happened\"},\n\t\t\"dash\":      {arg: \"$col0\", e: \"fred\"},\n\t\t\"underline\": {arg: \"$RESOURCE_GROUP\", e: \"foo\"},\n\t\t\"mix\":       {arg: \"$col0 and then $a but $B\", e: \"fred and then 10 but blee\"},\n\t\t\"subs\":      {arg: `{\"spec\" : {\"suspend\" : $COL0 }}`, e: `{\"spec\" : {\"suspend\" : fred }}`},\n\t\t\"boolean\":   {arg: \"$COL-BOOL\", e: \"false\"},\n\t\t\"invert\":    {arg: \"$!COL-BOOL\", e: \"true\"},\n\n\t\t\"simple_braces\":    {arg: \"${A}\", e: \"10\"},\n\t\t\"embed_braces\":     {arg: \"blabla${A}blabla\", e: \"blabla10blabla\"},\n\t\t\"open_braces\":      {arg: \"${A\", e: \"${A\"},\n\t\t\"closed_braces\":    {arg: \"$A}\", e: \"10}\"},\n\t\t\"substring_braces\": {arg: \"${A} and ${AA}\", e: \"10 and 20\"},\n\t\t\"with-text_braces\": {arg: \"Something ${A}\", e: \"Something 10\"},\n\t\t\"noMatch_braces\":   {arg: \"blah blah and ${BLEE}\", e: \"blah blah and ${BLEE}\"},\n\t\t\"lower_braces\":     {arg: \"And then ${b} happened\", e: \"And then blee happened\"},\n\t\t\"dash_braces\":      {arg: \"${col0}\", e: \"fred\"},\n\t\t\"underline_braces\": {arg: \"${RESOURCE_GROUP}\", e: \"foo\"},\n\t\t\"mix_braces\":       {arg: \"${col0} and then ${a} but ${B}\", e: \"fred and then 10 but blee\"},\n\t\t\"subs_braces\":      {arg: `{\"spec\" : {\"suspend\" : ${COL0} }}`, e: `{\"spec\" : {\"suspend\" : fred }}`},\n\t\t\"boolean_braces\":   {arg: \"${COL-BOOL}\", e: \"false\"},\n\t\t\"invert_braces\":    {arg: \"${!COL-BOOL}\", e: \"true\"},\n\t\t\"special_braces\":   {arg: \"${COL-%CPU/L}/${COL-MEM/R:L}\", e: \"10/32:32\"},\n\t\t\"space_braces\":     {arg: \"${READINESS GATES}\", e: \"bar\"},\n\t}\n\n\te := Env{\n\t\t\"A\":               \"10\",\n\t\t\"AA\":              \"20\",\n\t\t\"B\":               \"blee\",\n\t\t\"COL0\":            \"fred\",\n\t\t\"FRED\":            \"fred\",\n\t\t\"COL-NAME\":        \"zorg\",\n\t\t\"COL-BOOL\":        \"false\",\n\t\t\"COL-%CPU/L\":      \"10\",\n\t\t\"COL-MEM/R:L\":     \"32:32\",\n\t\t\"RESOURCE_GROUP\":  \"foo\",\n\t\t\"READINESS GATES\": \"bar\",\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\ta, err := e.Substitute(u.arg)\n\t\t\tassert.Equal(t, u.err, err)\n\t\t\tassert.Equal(t, u.e, a)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/view/event.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\nconst faultsFilter = \"Warning|Error\"\n\n// Event represents a command alias view.\ntype Event struct {\n\tResourceViewer\n}\n\n// NewEvent returns a new alias view.\nfunc NewEvent(gvr *client.GVR) ResourceViewer {\n\te := Event{\n\t\tResourceViewer: NewBrowser(gvr),\n\t}\n\te.AddBindKeysFn(e.bindKeys)\n\te.GetTable().SetSortCol(\"LAST SEEN\", false)\n\n\treturn &e\n}\n\nfunc (e *Event) bindKeys(aa *ui.KeyActions) {\n\taa.Delete(tcell.KeyCtrlD, ui.KeyE, ui.KeyA)\n\taa.Bulk(ui.KeyMap{\n\t\ttcell.KeyCtrlZ: ui.NewKeyAction(\"Toggle Faults\", e.toggleFaults, false),\n\t})\n}\n\nfunc (e *Event) toggleFaults(*tcell.EventKey) *tcell.EventKey {\n\tb, ok := e.ResourceViewer.(*Browser)\n\tif !ok {\n\t\treturn nil\n\t}\n\tfilter := b.CmdBuff().GetText()\n\tif filter == faultsFilter {\n\t\te.SetFilter(\"\", true)\n\t\te.App().Flash().Info(\"Showing all events\")\n\t} else {\n\t\te.SetFilter(faultsFilter, true)\n\t\te.App().Flash().Info(\"Showing Warning and Error events only\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/view/exec.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"os\"\n\t\"os/exec\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui/dialog\"\n\t\"github.com/fatih/color\"\n\tv1 \"k8s.io/api/core/v1\"\n\tkerrors \"k8s.io/apimachinery/pkg/api/errors\"\n\t\"k8s.io/apimachinery/pkg/api/resource\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nconst (\n\tshellCheck   = `command -v bash >/dev/null && exec bash || exec sh`\n\tbannerFmt    = \"<<K9s-Shell>> Pod: %s | Container: %s \\n\"\n\toutputPrefix = \"[output]\"\n)\n\nvar editorEnvVars = []string{\"K9S_EDITOR\", \"KUBE_EDITOR\", \"EDITOR\"}\n\ntype shellOpts struct {\n\tclear, background bool\n\tpipes             []string\n\tbinary            string\n\tbanner            string\n\targs              []string\n}\n\nfunc (s shellOpts) String() string {\n\treturn fmt.Sprintf(\"%s %s\", s.binary, strings.Join(s.args, \" \"))\n}\n\nfunc runK(a *App, opts *shellOpts) error {\n\tbin, err := exec.LookPath(\"kubectl\")\n\tif errors.Is(err, exec.ErrDot) {\n\t\treturn fmt.Errorf(\"kubectl command must not be in the current working directory: %w\", err)\n\t}\n\tif err != nil {\n\t\treturn fmt.Errorf(\"kubectl command is not in your path: %w\", err)\n\t}\n\targs := []string{opts.args[0]}\n\tif u, err := a.Conn().Config().ImpersonateUser(); err == nil {\n\t\targs = append(args, \"--as\", u)\n\t}\n\tif g, err := a.Conn().Config().ImpersonateGroups(); err == nil {\n\t\targs = append(args, \"--as-group\", g)\n\t}\n\tif isInsecure := a.Conn().Config().Flags().Insecure; isInsecure != nil && *isInsecure {\n\t\targs = append(args, \"--insecure-skip-tls-verify\")\n\t}\n\targs = append(args, \"--context\", a.Config.K9s.ActiveContextName())\n\tif cfg := a.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != \"\" {\n\t\targs = append(args, \"--kubeconfig\", *cfg)\n\t}\n\tif len(args) > 0 {\n\t\topts.args = append(args, opts.args[1:]...)\n\t}\n\topts.binary = bin\n\n\tsuspended, errChan, stChan := run(a, opts)\n\tif !suspended {\n\t\treturn fmt.Errorf(\"unable to run command\")\n\t}\n\tfor v := range stChan {\n\t\tslog.Debug(\"stdout\", slogs.Line, v)\n\t}\n\tvar errs error\n\tfor e := range errChan {\n\t\terrs = errors.Join(errs, e)\n\t}\n\n\treturn errs\n}\n\nfunc run(a *App, opts *shellOpts) (ok bool, errC chan error, outC chan string) {\n\terrChan := make(chan error, 1)\n\tstatusChan := make(chan string, 1)\n\n\tif opts.background {\n\t\tif err := execute(opts, statusChan); err != nil {\n\t\t\terrChan <- err\n\t\t\ta.Flash().Errf(\"Exec failed %q: %s\", opts, err)\n\t\t}\n\t\tclose(errChan)\n\t\treturn true, errChan, statusChan\n\t}\n\n\ta.Halt()\n\tdefer a.Resume()\n\n\treturn a.Suspend(func() {\n\t\tif err := execute(opts, statusChan); err != nil {\n\t\t\terrChan <- err\n\t\t\ta.Flash().Errf(\"Exec failed %q: %s\", opts, err)\n\t\t}\n\t\tclose(errChan)\n\t}), errChan, statusChan\n}\n\nfunc edit(a *App, opts *shellOpts) bool {\n\tvar (\n\t\tbin string\n\t\terr error\n\t)\n\tfor _, e := range editorEnvVars {\n\t\tenv := os.Getenv(e)\n\t\tif env == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// There may be situations where the user sets the editor as the binary\n\t\t// followed by some arguments (e.g. \"code -w\" to make it work with vscode)\n\t\t//\n\t\t// In such cases, the actual binary is only the first token\n\t\tenvTokens := strings.Split(env, \" \")\n\n\t\tif bin, err = exec.LookPath(envTokens[0]); err == nil {\n\t\t\t// Make sure the path is at the end (this allows running editors\n\t\t\t// with custom options)\n\t\t\tif len(envTokens) > 1 {\n\t\t\t\toriginalArgs := opts.args\n\t\t\t\topts.args = envTokens[1:]\n\t\t\t\topts.args = append(opts.args, originalArgs...)\n\t\t\t}\n\n\t\t\tbreak\n\t\t}\n\t}\n\tif bin == \"\" {\n\t\ta.Flash().Errf(\"You must set at least one of those env vars: %s\", strings.Join(editorEnvVars, \"|\"))\n\t\treturn false\n\t}\n\topts.binary, opts.background = bin, false\n\n\tsuspended, errChan, _ := run(a, opts)\n\tif !suspended {\n\t\ta.Flash().Errf(\"edit command failed\")\n\t}\n\tstatus := true\n\tfor e := range errChan {\n\t\ta.Flash().Err(e)\n\t\tstatus = false\n\t}\n\n\treturn status\n}\n\nfunc execute(opts *shellOpts, statusChan chan<- string) error {\n\tif opts.clear {\n\t\tclearScreen()\n\t}\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer func() {\n\t\tif !opts.background {\n\t\t\tcancel()\n\t\t\tclearScreen()\n\t\t}\n\t}()\n\n\tvar interrupted bool\n\tsigChan := make(chan os.Signal, 1)\n\tsignal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)\n\tgo func(cancel context.CancelFunc) {\n\t\tdefer slog.Debug(\"Got signal canceled\")\n\t\tselect {\n\t\tcase sig := <-sigChan:\n\t\t\tslog.Debug(\"Command canceled with signal\", slogs.Sig, sig)\n\t\t\tcancel()\n\t\tcase <-ctx.Done():\n\t\t\tslog.Debug(\"Signal context canceled!\")\n\t\t}\n\t\tinterrupted = true\n\t}(cancel)\n\n\tcmds := make([]*exec.Cmd, 0, 1)\n\tcmd := exec.CommandContext(ctx, opts.binary, opts.args...)\n\tslog.Debug(\"Exec command\", slogs.Command, opts)\n\n\tif env := os.Getenv(\"K9S_EDITOR\"); env != \"\" {\n\t\t// There may be situations where the user sets the editor as the binary\n\t\t// followed by some arguments (e.g. \"code -w\" to make it work with vscode)\n\t\t//\n\t\t// In such cases, the actual binary is only the first token\n\t\tbinTokens := strings.Split(env, \" \")\n\n\t\tif bin, err := exec.LookPath(binTokens[0]); err == nil {\n\t\t\tbinTokens[0] = bin\n\t\t\tcmd.Env = append(os.Environ(), fmt.Sprintf(\"KUBE_EDITOR=%s\", strings.Join(binTokens, \" \")))\n\t\t}\n\t}\n\n\tcmds = append(cmds, cmd)\n\n\tfor _, p := range opts.pipes {\n\t\ttokens := strings.Split(p, \" \")\n\t\tif len(tokens) < 2 {\n\t\t\tcontinue\n\t\t}\n\t\tcmd := exec.CommandContext(ctx, tokens[0], tokens[1:]...)\n\t\tslog.Debug(\"Exec command\", slogs.Command, cmd)\n\t\tcmds = append(cmds, cmd)\n\t}\n\n\tvar o, e bytes.Buffer\n\terr := pipe(ctx, opts, statusChan, &o, &e, cmds...)\n\tif err != nil && !interrupted {\n\t\tslog.Error(\"Pipe Exec failed\",\n\t\t\tslogs.Error, err,\n\t\t\tslogs.Command, cmds,\n\t\t)\n\t\treturn errors.Join(err, fmt.Errorf(\"%s\", e.String()))\n\t}\n\n\treturn nil\n}\n\nfunc runKu(ctx context.Context, a *App, opts *shellOpts) (string, error) {\n\tbin, err := exec.LookPath(\"kubectl\")\n\tif errors.Is(err, exec.ErrDot) {\n\t\tslog.Error(\"Kubectl exec can not reside in current working directory\", slogs.Error, err)\n\t\treturn \"\", err\n\t}\n\tif err != nil {\n\t\tslog.Error(\"Kubectl exec not found\", slogs.Error, err)\n\t\treturn \"\", err\n\t}\n\tvar args []string\n\tif u, err := a.Conn().Config().ImpersonateUser(); err == nil {\n\t\targs = append(args, \"--as\", u)\n\t}\n\tif g, err := a.Conn().Config().ImpersonateGroups(); err == nil {\n\t\targs = append(args, \"--as-group\", g)\n\t}\n\targs = append(args, \"--context\", a.Config.K9s.ActiveContextName())\n\tif cfg := a.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != \"\" {\n\t\targs = append(args, \"--kubeconfig\", *cfg)\n\t}\n\tif len(args) > 0 {\n\t\topts.args = append(args, opts.args...)\n\t}\n\topts.binary, opts.background = bin, false\n\n\treturn oneShoot(ctx, opts)\n}\n\nfunc oneShoot(ctx context.Context, opts *shellOpts) (string, error) {\n\tif opts.clear {\n\t\tclearScreen()\n\t}\n\n\tslog.Debug(\"Executing command\",\n\t\tslogs.Bin, opts.binary,\n\t\tslogs.Args, strings.Join(opts.args, \" \"),\n\t)\n\tcmd := exec.CommandContext(ctx, opts.binary, opts.args...)\n\n\tvar err error\n\tbuff := bytes.NewBufferString(\"\")\n\tcmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, buff, buff\n\t_, _ = cmd.Stdout.Write([]byte(opts.banner))\n\terr = cmd.Run()\n\n\treturn strings.Trim(buff.String(), \"\\n\"), err\n}\n\nfunc clearScreen() {\n\tfmt.Print(\"\\033[H\\033[2J\")\n}\n\nconst (\n\tk9sShell           = \"k9s-shell\"\n\tk9sShellRetryCount = 50\n\tk9sShellRetryDelay = 2 * time.Second\n)\n\nfunc launchNodeShell(v model.Igniter, a *App, node string) {\n\tif err := nukeK9sShell(a); err != nil {\n\t\ta.Flash().Errf(\"Cleaning node shell failed: %s\", err)\n\t\treturn\n\t}\n\n\tmsg := fmt.Sprintf(\"Launching node shell on %s...\", node)\n\td := a.Styles.Dialog()\n\tdialog.ShowPrompt(&d, a.Content.Pages, \"Launching\", msg, func(ctx context.Context) {\n\t\terr := launchShellPod(ctx, a, node)\n\t\tif err != nil {\n\t\t\tif !errors.Is(err, context.Canceled) {\n\t\t\t\ta.Flash().Errf(\"Launching node shell failed: %s\", err)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tgo launchPodShell(v, a)\n\t}, func() {\n\t\tif err := nukeK9sShell(a); err != nil {\n\t\t\ta.Flash().Errf(\"Cleaning node shell failed: %s\", err)\n\t\t\treturn\n\t\t}\n\t})\n}\n\nfunc launchPodShell(v model.Igniter, a *App) {\n\tif a.Config.K9s.ShellPod == nil {\n\t\tslog.Error(\"Shell pod not configured!\")\n\t\treturn\n\t}\n\n\tdefer func() {\n\t\tif err := nukeK9sShell(a); err != nil {\n\t\t\ta.Flash().Errf(\"Launching node shell failed: %s\", err)\n\t\t\treturn\n\t\t}\n\t}()\n\n\tv.Stop()\n\tdefer v.Start()\n\n\tns := a.Config.K9s.ShellPod.Namespace\n\tif err := sshIn(a, client.FQN(ns, k9sShellPodName()), k9sShell); err != nil {\n\t\ta.Flash().Errf(\"Launching node shell failed: %s\", err)\n\t}\n}\n\nfunc sshIn(a *App, fqn, co string) error {\n\tcfg := a.Config.K9s.ShellPod\n\tplatform, err := getPodOS(a.factory, fqn)\n\tif err != nil {\n\t\tslog.Warn(\"os detect failed\", slogs.Error, err)\n\t}\n\n\targs := buildShellArgs(\"exec\", fqn, co, a.Conn().Config().Flags())\n\targs = append(args, \"--\")\n\tif len(cfg.Command) > 0 {\n\t\targs = append(args, cfg.Command...)\n\t\targs = append(args, cfg.Args...)\n\t} else {\n\t\tif platform == windowsOS {\n\t\t\targs = append(args, \"--\", powerShell)\n\t\t}\n\t\targs = append(args, \"sh\", \"-c\", shellCheck)\n\t}\n\tslog.Debug(\"Running command with args\", slogs.Args, args)\n\n\tc := color.New(color.BgGreen).Add(color.FgBlack).Add(color.Bold)\n\terr = runK(a, &shellOpts{\n\t\tclear:  true,\n\t\tbanner: c.Sprintf(bannerFmt, fqn, co),\n\t\targs:   args},\n\t)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"shell exec failed: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc nukeK9sShell(a *App) error {\n\tct, err := a.Config.K9s.ActiveContext()\n\tif err != nil {\n\t\treturn err\n\t}\n\tif !ct.FeatureGates.NodeShell || a.Config.K9s.ShellPod == nil {\n\t\treturn nil\n\t}\n\n\tns := a.Config.K9s.ShellPod.Namespace\n\tctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)\n\tdefer cancel()\n\n\tdial, err := a.Conn().Dial()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = dial.CoreV1().Pods(ns).Delete(ctx, k9sShellPodName(), metav1.DeleteOptions{})\n\tif kerrors.IsNotFound(err) {\n\t\treturn nil\n\t}\n\n\treturn err\n}\n\nfunc launchShellPod(ctx context.Context, a *App, node string) error {\n\tvar (\n\t\tspo  = a.Config.K9s.ShellPod\n\t\tspec = k9sShellPod(node, spo)\n\t)\n\n\tdial, err := a.Conn().Dial()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tconn := dial.CoreV1().Pods(spo.Namespace)\n\tif _, err = conn.Create(ctx, spec, metav1.CreateOptions{}); err != nil {\n\t\treturn err\n\t}\n\n\tfor i := range k9sShellRetryCount {\n\t\to, err := a.factory.Get(client.PodGVR, client.FQN(spo.Namespace, k9sShellPodName()), true, labels.Everything())\n\t\tif err != nil {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn ctx.Err()\n\t\t\tcase <-time.After(k9sShellRetryDelay):\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tvar pod v1.Pod\n\t\tif err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tslog.Debug(\"Checking k9s shell pod retries\",\n\t\t\tslogs.Retry, i,\n\t\t\tslogs.PodPhase, pod.Status.Phase,\n\t\t)\n\t\tif pod.Status.Phase == v1.PodRunning {\n\t\t\treturn nil\n\t\t}\n\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tcase <-time.After(k9sShellRetryDelay):\n\t\t}\n\t}\n\n\treturn fmt.Errorf(\"unable to launch shell pod on node %s\", node)\n}\n\nfunc k9sShellPodName() string {\n\treturn fmt.Sprintf(\"%s-%d\", k9sShell, os.Getpid())\n}\n\nfunc k9sShellPod(node string, cfg *config.ShellPod) *v1.Pod {\n\tvar grace int64\n\tvar priv = true\n\n\tslog.Debug(\"Shell pod config\", slogs.ShellPodCfg, cfg)\n\tc := v1.Container{\n\t\tName:            k9sShell,\n\t\tImage:           cfg.Image,\n\t\tImagePullPolicy: cfg.ImagePullPolicy,\n\t\tVolumeMounts: []v1.VolumeMount{\n\t\t\t{\n\t\t\t\tName:      \"root-vol\",\n\t\t\t\tMountPath: \"/host\",\n\t\t\t\tReadOnly:  true,\n\t\t\t},\n\t\t},\n\t\tResources: asResource(cfg.Limits),\n\t\tStdin:     true,\n\t\tTTY:       cfg.TTY,\n\t\tSecurityContext: &v1.SecurityContext{\n\t\t\tPrivileged: &priv,\n\t\t},\n\t}\n\tv := []v1.Volume{\n\t\t{\n\t\t\tName: \"root-vol\",\n\t\t\tVolumeSource: v1.VolumeSource{\n\t\t\t\tHostPath: &v1.HostPathVolumeSource{\n\t\t\t\t\tPath: \"/\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\tif len(cfg.Command) != 0 {\n\t\tc.Command = cfg.Command\n\t}\n\tif len(cfg.Args) > 0 {\n\t\tc.Args = cfg.Args\n\t}\n\tif len(cfg.HostPathVolume) > 0 {\n\t\tfor _, h := range cfg.HostPathVolume {\n\t\t\tc.VolumeMounts = append(c.VolumeMounts, v1.VolumeMount{\n\t\t\t\tName:      h.Name,\n\t\t\t\tMountPath: h.MountPath,\n\t\t\t\tReadOnly:  h.ReadOnly,\n\t\t\t})\n\t\t\tv = append(v, v1.Volume{\n\t\t\t\tName: h.Name,\n\t\t\t\tVolumeSource: v1.VolumeSource{\n\t\t\t\t\tHostPath: &v1.HostPathVolumeSource{\n\t\t\t\t\t\tPath: h.HostPath,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\t}\n\treturn &v1.Pod{\n\t\tObjectMeta: metav1.ObjectMeta{\n\t\t\tName:      k9sShellPodName(),\n\t\t\tNamespace: cfg.Namespace,\n\t\t\tLabels:    cfg.Labels,\n\t\t},\n\t\tSpec: v1.PodSpec{\n\t\t\tNodeName:                      node,\n\t\t\tRestartPolicy:                 v1.RestartPolicyNever,\n\t\t\tHostPID:                       true,\n\t\t\tHostNetwork:                   true,\n\t\t\tImagePullSecrets:              cfg.ImagePullSecrets,\n\t\t\tTerminationGracePeriodSeconds: &grace,\n\t\t\tVolumes:                       v,\n\t\t\tContainers:                    []v1.Container{c},\n\t\t\tTolerations: []v1.Toleration{\n\t\t\t\t{\n\t\t\t\t\tOperator: v1.TolerationOperator(\"Exists\"),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc asResource(r config.Limits) v1.ResourceRequirements {\n\treturn v1.ResourceRequirements{\n\t\tLimits: v1.ResourceList{\n\t\t\tv1.ResourceCPU:    resource.MustParse(r[v1.ResourceCPU]),\n\t\t\tv1.ResourceMemory: resource.MustParse(r[v1.ResourceMemory]),\n\t\t},\n\t}\n}\n\nfunc pipe(_ context.Context, opts *shellOpts, statusChan chan<- string, w, e *bytes.Buffer, cmds ...*exec.Cmd) error {\n\tif len(cmds) == 0 {\n\t\treturn nil\n\t}\n\n\tif len(cmds) == 1 {\n\t\tcmd := cmds[0]\n\t\tif opts.background {\n\t\t\tgo func() {\n\t\t\t\tcmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, w, e\n\t\t\t\tif err := cmd.Run(); err != nil {\n\t\t\t\t\tslog.Error(\"Command exec failed\", slogs.Error, err)\n\t\t\t\t} else {\n\t\t\t\t\tfor _, l := range strings.Split(w.String(), \"\\n\") {\n\t\t\t\t\t\tif l != \"\" {\n\t\t\t\t\t\t\tstatusChan <- fmt.Sprintf(\"%s %s\", outputPrefix, l)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tstatusChan <- fmt.Sprintf(\"Command completed successfully: %q\", render.Truncate(cmd.String(), 20))\n\t\t\t\t\tslog.Info(\"Command ran successfully\", slogs.Command, cmd.String())\n\t\t\t\t}\n\t\t\t\tclose(statusChan)\n\t\t\t}()\n\t\t\treturn nil\n\t\t}\n\t\tcmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr\n\t\t_, _ = cmd.Stdout.Write([]byte(opts.banner))\n\n\t\tslog.Debug(\"Exec started\")\n\t\terr := cmd.Run()\n\t\tvar ex *exec.ExitError\n\t\t// Check if exec failed from a signal\n\t\tif errors.As(err, &ex) && !ex.Exited() {\n\t\t\treturn nil\n\t\t}\n\t\tslog.Debug(\"Command exec done\", slogs.Error, err)\n\t\tif err == nil {\n\t\t\tstatusChan <- fmt.Sprintf(\"Command completed successfully: %q\", cmd.String())\n\t\t}\n\t\tclose(statusChan)\n\n\t\tif err != nil {\n\t\t\terr = fmt.Errorf(\"command failed. Check k9s logs: %w\", err)\n\t\t}\n\n\t\treturn err\n\t}\n\n\tlast := len(cmds) - 1\n\tfor i := range cmds {\n\t\tcmds[i].Stderr = os.Stderr\n\t\tif i+1 < len(cmds) {\n\t\t\tr, w := io.Pipe()\n\t\t\tcmds[i].Stdout, cmds[i+1].Stdin = w, r\n\t\t}\n\t}\n\tcmds[last].Stdout = os.Stdout\n\n\tfor _, cmd := range cmds {\n\t\tslog.Debug(\"Starting command\", slogs.Command, cmd)\n\t\tif err := cmd.Start(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn cmds[len(cmds)-1].Wait()\n}\n"
  },
  {
    "path": "internal/view/group.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\n// Group presents a RBAC group viewer.\ntype Group struct {\n\tResourceViewer\n}\n\n// NewGroup returns a new subject viewer.\nfunc NewGroup(gvr *client.GVR) ResourceViewer {\n\tg := Group{ResourceViewer: NewBrowser(gvr)}\n\tg.AddBindKeysFn(g.bindKeys)\n\tg.SetContextFn(g.subjectCtx)\n\n\treturn &g\n}\n\nfunc (g *Group) bindKeys(aa *ui.KeyActions) {\n\taa.Delete(ui.KeyShiftA, ui.KeyShiftP, tcell.KeyCtrlSpace, ui.KeySpace)\n\taa.Bulk(ui.KeyMap{\n\t\ttcell.KeyEnter: ui.NewKeyAction(\"Rules\", g.policyCmd, true),\n\t})\n}\n\nfunc (*Group) subjectCtx(ctx context.Context) context.Context {\n\treturn context.WithValue(ctx, internal.KeySubjectKind, \"Group\")\n}\n\nfunc (g *Group) policyCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := g.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\tif err := g.App().inject(NewPolicy(g.App(), \"Group\", path), false); err != nil {\n\t\tg.App().Flash().Err(err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/view/helm_chart.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\n// HelmChart represents a helm chart view.\ntype HelmChart struct {\n\tResourceViewer\n}\n\n// NewHelmChart returns a new helm-chart view.\nfunc NewHelmChart(gvr *client.GVR) ResourceViewer {\n\tc := HelmChart{\n\t\tResourceViewer: NewValueExtender(NewBrowser(gvr)),\n\t}\n\tc.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen)\n\tc.GetTable().SetSelectedStyle(tcell.StyleDefault.\n\t\tForeground(tcell.ColorWhite).\n\t\tBackground(tcell.ColorMediumSpringGreen).Attributes(tcell.AttrNone))\n\tc.AddBindKeysFn(c.bindKeys)\n\tc.GetTable().SetEnterFn(c.viewReleases)\n\tc.SetContextFn(c.chartContext)\n\n\treturn &c\n}\n\nfunc (*HelmChart) chartContext(ctx context.Context) context.Context {\n\treturn ctx\n}\n\nfunc (c *HelmChart) bindKeys(aa *ui.KeyActions) {\n\taa.Delete(tcell.KeyCtrlS)\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyR: ui.NewKeyAction(\"Releases\", c.historyCmd, true),\n\t})\n}\n\nfunc (c *HelmChart) viewReleases(app *App, _ ui.Tabular, _ *client.GVR, _ string) {\n\tv := NewHistory(client.HmhGVR)\n\tv.SetContextFn(c.helmContext)\n\tif err := app.inject(v, false); err != nil {\n\t\tapp.Flash().Err(err)\n\t}\n}\n\nfunc (c *HelmChart) historyCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := c.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\tc.viewReleases(c.App(), c.GetTable().GetModel(), c.GVR(), path)\n\n\treturn nil\n}\n\nfunc (c *HelmChart) helmContext(ctx context.Context) context.Context {\n\tpath := c.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn ctx\n\t}\n\tctx = context.WithValue(ctx, internal.KeyFQN, path)\n\n\treturn context.WithValue(ctx, internal.KeyPath, path)\n}\n"
  },
  {
    "path": "internal/view/helm_history.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/render/helm\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/ui/dialog\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\n// History represents a helm History view.\ntype History struct {\n\tResourceViewer\n\n\tValues *model.RevValues\n}\n\n// NewHistory returns a new helm-history view.\nfunc NewHistory(gvr *client.GVR) ResourceViewer {\n\th := History{\n\t\tResourceViewer: NewValueExtender(NewBrowser(gvr)),\n\t}\n\th.GetTable().SetColorerFn(helm.History{}.ColorerFunc())\n\th.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen)\n\th.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorMediumSpringGreen).Attributes(tcell.AttrNone))\n\th.AddBindKeysFn(h.bindKeys)\n\th.SetContextFn(h.HistoryContext)\n\th.GetTable().SetEnterFn(h.getValsCmd)\n\n\treturn &h\n}\n\n// Init initializes the view\nfunc (h *History) Init(ctx context.Context) error {\n\tif err := h.ResourceViewer.Init(ctx); err != nil {\n\t\treturn err\n\t}\n\th.GetTable().SetSortCol(\"REVISION\", false)\n\n\treturn nil\n}\n\nfunc (*History) HistoryContext(ctx context.Context) context.Context {\n\treturn ctx\n}\n\nfunc (h *History) bindKeys(aa *ui.KeyActions) {\n\tif !h.App().Config.IsReadOnly() {\n\t\th.bindDangerousKeys(aa)\n\t}\n\n\taa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace, tcell.KeyCtrlD)\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyShiftN: ui.NewKeyAction(\"Sort Revision\", h.GetTable().SortColCmd(\"REVISION\", true), false),\n\t\tui.KeyShiftA: ui.NewKeyAction(\"Sort Age\", h.GetTable().SortColCmd(\"AGE\", true), false),\n\t})\n}\n\nfunc (h *History) getValsCmd(app *App, _ ui.Tabular, _ *client.GVR, path string) {\n\tns, n := client.Namespaced(path)\n\ttt := strings.Split(n, \":\")\n\tif len(tt) < 2 {\n\t\tapp.Flash().Err(fmt.Errorf(\"unable to parse version in %q\", path))\n\t\treturn\n\t}\n\tname, rev := tt[0], tt[1]\n\th.Values = model.NewRevValues(h.GVR(), client.FQN(ns, name), rev)\n\tv := NewLiveView(h.App(), \"Values\", h.Values)\n\tif err := v.app.inject(v, false); err != nil {\n\t\tv.app.Flash().Err(err)\n\t}\n}\n\nfunc (h *History) bindDangerousKeys(aa *ui.KeyActions) {\n\taa.Add(ui.KeyR, ui.NewKeyActionWithOpts(\"RollBackTo...\", h.rollbackCmd,\n\t\tui.ActionOpts{\n\t\t\tVisible:   true,\n\t\t\tDangerous: true,\n\t\t},\n\t))\n}\n\nfunc (h *History) rollbackCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := h.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\n\tns, nrev := client.Namespaced(path)\n\ttt := strings.Split(nrev, \":\")\n\tn, rev := nrev, \"\"\n\tif len(tt) == 2 {\n\t\tn, rev = tt[0], tt[1]\n\t}\n\n\th.Stop()\n\tdefer h.Start()\n\tmsg := fmt.Sprintf(\"RollingBack chart [yellow::b]%s[-::-] to release <[orangered::b]%s[-::-]>?\", n, rev)\n\tdialog.ShowConfirmAck(h.App().App, h.App().Content.Pages, n, false, \"Confirm Rollback\", msg, func() {\n\t\tctx, cancel := context.WithTimeout(context.Background(), h.App().Conn().Config().CallTimeout())\n\t\tdefer cancel()\n\t\tif err := h.rollback(ctx, client.FQN(ns, n), rev); err != nil {\n\t\t\th.App().Flash().Err(err)\n\t\t} else {\n\t\t\th.App().Flash().Infof(\"Rollout restart in progress for char `%s...\", n)\n\t\t}\n\t}, func() {})\n\n\treturn nil\n}\n\nfunc (h *History) rollback(ctx context.Context, path, rev string) error {\n\tvar hm dao.HelmHistory\n\thm.Init(h.App().factory, h.GVR())\n\tif err := hm.Rollback(ctx, path, rev); err != nil {\n\t\treturn err\n\t}\n\th.Refresh()\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/view/help.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strconv\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n)\n\nconst (\n\thelpTitle    = \"Help\"\n\thelpTitleFmt = \" [aqua::b]%s \"\n)\n\n// HelpFunc processes menu hints.\ntype HelpFunc func() model.MenuHints\n\n// Help presents a help viewer.\ntype Help struct {\n\t*Table\n\n\tstyles                   *config.Styles\n\thints                    HelpFunc\n\tmaxKey, maxDesc, maxRows int\n}\n\n// NewHelp returns a new help viewer.\nfunc NewHelp(app *App) *Help {\n\treturn &Help{\n\t\tTable: NewTable(client.HlpGVR),\n\t\thints: app.Content.Top().Hints,\n\t}\n}\n\nfunc (*Help) SetCommand(*cmd.Interpreter)            {}\nfunc (*Help) SetFilter(string, bool)                 {}\nfunc (*Help) SetLabelSelector(labels.Selector, bool) {}\n\n// Init initializes the component.\nfunc (h *Help) Init(ctx context.Context) error {\n\tif err := h.Table.Init(ctx); err != nil {\n\t\treturn err\n\t}\n\th.SetSelectable(false, false)\n\th.resetTitle()\n\th.SetBorder(true)\n\th.SetBorderPadding(0, 0, 1, 1)\n\th.bindKeys()\n\th.build()\n\th.app.Styles.AddListener(h)\n\th.StylesChanged(h.app.Styles)\n\n\treturn nil\n}\n\n// InCmdMode checks if prompt is active.\nfunc (*Help) InCmdMode() bool {\n\treturn false\n}\n\n// StylesChanged notifies skin changed.\nfunc (h *Help) StylesChanged(s *config.Styles) {\n\th.styles = s\n\th.SetBackgroundColor(s.BgColor())\n\th.updateStyle()\n}\n\nfunc (h *Help) bindKeys() {\n\th.Actions().Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlS, ui.KeySlash)\n\th.Actions().Bulk(ui.KeyMap{\n\t\ttcell.KeyEscape: ui.NewKeyAction(\"Back\", h.app.PrevCmd, true),\n\t\tui.KeyQ:         ui.NewKeyAction(\"Back\", h.app.PrevCmd, false),\n\t\tui.KeyHelp:      ui.NewKeyAction(\"Back\", h.app.PrevCmd, false),\n\t\ttcell.KeyEnter:  ui.NewKeyAction(\"Back\", h.app.PrevCmd, false),\n\t})\n}\n\nfunc (h *Help) computeMaxes(hh model.MenuHints) {\n\th.maxKey, h.maxDesc = 0, 0\n\tfor _, hint := range hh {\n\t\tif len(hint.Mnemonic) > h.maxKey {\n\t\t\th.maxKey = len(hint.Mnemonic)\n\t\t}\n\t\tif len(hint.Description) > h.maxDesc {\n\t\t\th.maxDesc = len(hint.Description)\n\t\t}\n\t}\n\th.maxKey += 2\n}\n\nfunc (h *Help) computeExtraMaxes(ee map[string]string) {\n\tfor k, v := range ee {\n\t\tif len(k) > h.maxDesc {\n\t\t\th.maxDesc = len(k)\n\t\t}\n\t\tif len(v) > h.maxKey {\n\t\t\th.maxKey = len(v)\n\t\t}\n\t}\n}\n\nfunc (h *Help) build() {\n\th.Clear()\n\n\tsections := []string{\"RESOURCE\", \"GENERAL\", \"NAVIGATION\"}\n\th.maxRows = len(h.showGeneral())\n\tff := []HelpFunc{\n\t\th.hints,\n\t\th.showGeneral,\n\t\th.showNav,\n\t}\n\n\tvar col int\n\textras := h.app.Content.Top().ExtraHints()\n\tfor i, section := range sections {\n\t\thh := ff[i]()\n\t\tsort.Sort(hh)\n\t\th.computeMaxes(hh)\n\t\tif extras != nil {\n\t\t\th.computeExtraMaxes(extras)\n\t\t}\n\t\th.addSection(col, section, hh)\n\t\tif i == 0 && extras != nil {\n\t\t\th.addExtras(extras, col, len(hh))\n\t\t}\n\t\tcol += 2\n\t}\n\tif hh, err := h.showHotKeys(); err == nil {\n\t\th.computeMaxes(hh)\n\t\th.addSection(col, \"HOTKEYS\", hh)\n\t}\n}\n\nfunc (h *Help) addExtras(extras map[string]string, col, size int) {\n\tkk := make([]string, 0, len(extras))\n\tfor k := range extras {\n\t\tkk = append(kk, k)\n\t}\n\tsort.StringSlice(kk).Sort()\n\trow := size + 1\n\tfor _, k := range kk {\n\t\th.SetCell(row, col, padCell(extras[k], h.maxKey))\n\t\th.SetCell(row, col+1, padCell(k, h.maxDesc))\n\t\trow++\n\t}\n}\n\nfunc (*Help) showNav() model.MenuHints {\n\treturn model.MenuHints{\n\t\t{\n\t\t\tMnemonic:    \"g\",\n\t\t\tDescription: \"Goto Top\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"Shift-g\",\n\t\t\tDescription: \"Goto Bottom\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"Ctrl-b\",\n\t\t\tDescription: \"Page Up\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"Ctrl-f\",\n\t\t\tDescription: \"Page Down\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"h\",\n\t\t\tDescription: \"Left\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"l\",\n\t\t\tDescription: \"Right\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"k\",\n\t\t\tDescription: \"Up\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"j\",\n\t\t\tDescription: \"Down\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"[\",\n\t\t\tDescription: \"History Back\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"]\",\n\t\t\tDescription: \"History Forward\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"-\",\n\t\t\tDescription: \"Last Used Command\",\n\t\t},\n\t}\n}\n\nfunc (h *Help) showHotKeys() (model.MenuHints, error) {\n\thh := config.NewHotKeys()\n\tif err := hh.Load(h.App().Config.ContextHotkeysPath()); err != nil {\n\t\treturn nil, fmt.Errorf(\"no hotkey configuration found\")\n\t}\n\tkk := make(sort.StringSlice, 0, len(hh.HotKey))\n\tfor k := range hh.HotKey {\n\t\tkk = append(kk, k)\n\t}\n\tkk.Sort()\n\tmm := make(model.MenuHints, 0, len(hh.HotKey))\n\tfor _, k := range kk {\n\t\tmm = append(mm, model.MenuHint{\n\t\t\tMnemonic:    hh.HotKey[k].ShortCut,\n\t\t\tDescription: hh.HotKey[k].Description,\n\t\t})\n\t}\n\n\treturn mm, nil\n}\n\nfunc (*Help) showGeneral() model.MenuHints {\n\treturn model.MenuHints{\n\t\t{\n\t\t\tMnemonic:    \"?\",\n\t\t\tDescription: \"Help\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"Ctrl-a\",\n\t\t\tDescription: \"Aliases\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \":cmd\",\n\t\t\tDescription: \"Command mode\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"/term\",\n\t\t\tDescription: \"Filter mode\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"esc\",\n\t\t\tDescription: \"Back/Clear\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"q\",\n\t\t\tDescription: \"Back\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"tab\",\n\t\t\tDescription: \"Field Next\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"backtab\",\n\t\t\tDescription: \"Field Previous\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"Ctrl-r\",\n\t\t\tDescription: \"Reload\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"Ctrl-u\",\n\t\t\tDescription: \"Command Clear\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"Ctrl-e\",\n\t\t\tDescription: \"Toggle Header\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"Ctrl-g\",\n\t\t\tDescription: \"Toggle Crumbs\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \":q\",\n\t\t\tDescription: \"Quit\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"space\",\n\t\t\tDescription: \"Mark\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"Ctrl-space\",\n\t\t\tDescription: \"Mark Range\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"Ctrl-\\\\\",\n\t\t\tDescription: \"Mark Clear\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"Ctrl-s\",\n\t\t\tDescription: \"Save\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"shift-left\",\n\t\t\tDescription: \"Select Previous Column\",\n\t\t},\n\t\t{\n\t\t\tMnemonic:    \"shift-right\",\n\t\t\tDescription: \"Select Next Column\",\n\t\t},\n\t}\n}\n\nfunc (h *Help) resetTitle() {\n\th.SetTitle(fmt.Sprintf(helpTitleFmt, helpTitle))\n}\n\nfunc (h *Help) addSpacer(c int) {\n\tcell := tview.NewTableCell(render.Pad(\"\", h.maxKey))\n\tcell.SetExpansion(1)\n\th.SetCell(0, c, cell)\n}\n\nfunc (h *Help) addSection(c int, title string, hh model.MenuHints) {\n\tif len(hh) > h.maxRows {\n\t\th.maxRows = len(hh)\n\t}\n\trow := 0\n\th.SetCell(row, c, h.titleCell(title))\n\th.addSpacer(c + 1)\n\trow++\n\n\tfor _, hint := range hh {\n\t\tcol := c\n\t\th.SetCell(row, col, padCellWithRef(ui.ToMnemonic(hint.Mnemonic), h.maxKey, hint.Mnemonic))\n\t\tcol++\n\t\th.SetCell(row, col, padCell(hint.Description, h.maxDesc))\n\t\trow++\n\t}\n\n\tif len(hh) >= h.maxRows {\n\t\treturn\n\t}\n\n\tfor i := h.maxRows - len(hh); i > 0; i-- {\n\t\tcol := c\n\t\th.SetCell(row, col, padCell(\"\", h.maxKey))\n\t\tcol++\n\t\th.SetCell(row, col, padCell(\"\", h.maxDesc))\n\t\trow++\n\t}\n}\n\nfunc (h *Help) updateStyle() {\n\tvar (\n\t\tstyle   = tcell.StyleDefault.Background(h.styles.K9s.Help.BgColor.Color())\n\t\tkey     = style.Foreground(h.styles.K9s.Help.KeyColor.Color()).Bold(true)\n\t\tnumKey  = style.Foreground(h.app.Styles.K9s.Help.NumKeyColor.Color()).Bold(true)\n\t\tinfo    = style.Foreground(h.app.Styles.K9s.Help.FgColor.Color())\n\t\theading = style.Foreground(h.app.Styles.K9s.Help.SectionColor.Color())\n\t)\n\tfor col := range h.GetColumnCount() {\n\t\tfor row := range h.GetRowCount() {\n\t\t\tc := h.GetCell(row, col)\n\t\t\tif c == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase row == 0:\n\t\t\t\tc.SetStyle(heading)\n\t\t\tcase col%2 != 0:\n\t\t\t\tc.SetStyle(info)\n\t\t\tdefault:\n\t\t\t\tif _, err := strconv.Atoi(extractRef(c)); err == nil {\n\t\t\t\t\tc.SetStyle(numKey)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tc.SetStyle(key)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc extractRef(c *tview.TableCell) string {\n\tif ref, ok := c.GetReference().(string); ok {\n\t\treturn ref\n\t}\n\n\treturn c.Text\n}\n\nfunc (h *Help) titleCell(title string) *tview.TableCell {\n\tc := tview.NewTableCell(title)\n\tc.SetTextColor(h.Styles().K9s.Help.SectionColor.Color())\n\tc.SetAttributes(tcell.AttrBold)\n\tc.SetExpansion(1)\n\tc.SetAlign(tview.AlignLeft)\n\n\treturn c\n}\n\nfunc padCellWithRef(s string, width int, ref any) *tview.TableCell {\n\treturn padCell(s, width).SetReference(ref)\n}\n\nfunc padCell(s string, width int) *tview.TableCell {\n\treturn tview.NewTableCell(render.Pad(s, width))\n}\n"
  },
  {
    "path": "internal/view/help_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestHelp(t *testing.T) {\n\tctx := makeCtx(t)\n\n\tapp := ctx.Value(internal.KeyApp).(*view.App)\n\tpo := view.NewPod(client.PodGVR)\n\trequire.NoError(t, po.Init(ctx))\n\tapp.Content.Push(po)\n\n\tv := view.NewHelp(app)\n\n\trequire.NoError(t, v.Init(ctx))\n\tassert.Equal(t, 20, v.GetRowCount())\n\tassert.Equal(t, 8, v.GetColumnCount())\n\tassert.Equal(t, \"<a>\", strings.TrimSpace(v.GetCell(1, 0).Text))\n\tassert.Equal(t, \"Attach\", strings.TrimSpace(v.GetCell(1, 1).Text))\n}\n"
  },
  {
    "path": "internal/view/helpers.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/atotto/clipboard\"\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\t\"github.com/sahilm/fuzzy\"\n\tv1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/util/sets\"\n)\n\nfunc isBailoutEvt(evt *tcell.EventKey) bool {\n\treturn evt.Name() == \"Ctrl+C\"\n}\n\nfunc aliases(m *v1.APIResource, aa sets.Set[string]) sets.Set[string] {\n\tss := sets.New(aa.UnsortedList()...)\n\tss.Insert(m.Name)\n\tss.Insert(m.ShortNames...)\n\tif m.SingularName != \"\" {\n\t\tss.Insert(m.SingularName)\n\t}\n\n\treturn ss\n}\n\nfunc clipboardWrite(text string) error {\n\tif text != \"\" {\n\t\treturn clipboard.WriteAll(text)\n\t}\n\n\treturn nil\n}\n\nvar bracketRX = regexp.MustCompile(`\\[(.+)\\[\\]`)\n\nfunc sanitizeEsc(s string) string {\n\treturn bracketRX.ReplaceAllString(s, `[$1]`)\n}\n\nfunc cpCmd(flash *model.Flash, v *tview.TextView) func(*tcell.EventKey) *tcell.EventKey {\n\treturn func(evt *tcell.EventKey) *tcell.EventKey {\n\t\tif err := clipboardWrite(sanitizeEsc(v.GetText(true))); err != nil {\n\t\t\tflash.Err(err)\n\t\t\treturn evt\n\t\t}\n\t\tflash.Info(\"Content copied to clipboard...\")\n\n\t\treturn nil\n\t}\n}\n\nfunc parsePFAnn(s string) (port, lport string, ok bool) {\n\ttokens := strings.Split(s, \":\")\n\tif len(tokens) != 2 {\n\t\treturn\n\t}\n\n\treturn tokens[0], tokens[1], true\n}\n\nfunc k8sEnv(c *client.Config) Env {\n\tctx, err := c.CurrentContextName()\n\tif err != nil {\n\t\tctx = render.NAValue\n\t}\n\tcluster, err := c.CurrentClusterName()\n\tif err != nil {\n\t\tcluster = render.NAValue\n\t}\n\tuser, err := c.CurrentUserName()\n\tif err != nil {\n\t\tuser = render.NAValue\n\t}\n\tgroups, err := c.CurrentGroupNames()\n\tif err != nil {\n\t\tgroups = []string{render.NAValue}\n\t}\n\n\tvar cfg string\n\tkcfg := c.Flags().KubeConfig\n\tif kcfg != nil && *kcfg != \"\" {\n\t\tcfg = *kcfg\n\t} else {\n\t\tcfg = os.Getenv(\"KUBECONFIG\")\n\t}\n\n\treturn Env{\n\t\t\"CONTEXT\":    ctx,\n\t\t\"CLUSTER\":    cluster,\n\t\t\"USER\":       user,\n\t\t\"GROUPS\":     strings.Join(groups, \",\"),\n\t\t\"KUBECONFIG\": cfg,\n\t}\n}\n\nfunc defaultEnv(c *client.Config, path string, header model1.Header, row *model1.Row) Env {\n\tenv := k8sEnv(c)\n\tenv[\"NAMESPACE\"], env[\"NAME\"] = client.Namespaced(path)\n\tif row == nil {\n\t\treturn env\n\t}\n\tfor _, col := range header.ColumnNames(true) {\n\t\tidx, ok := header.IndexOf(col, true)\n\t\tif ok && idx < len(row.Fields) {\n\t\t\tenv[\"COL-\"+col] = row.Fields[idx]\n\t\t}\n\t}\n\n\treturn env\n}\n\nfunc describeResource(app *App, _ ui.Tabular, gvr *client.GVR, path string) {\n\tv := NewLiveView(app, \"Describe\", model.NewDescribe(gvr, path))\n\tif err := app.inject(v, false); err != nil {\n\t\tapp.Flash().Err(err)\n\t}\n}\n\nfunc showReplicasets(app *App, path string, labelSel labels.Selector, fieldSel string) {\n\tv := NewReplicaSet(client.RsGVR)\n\tv.SetContextFn(func(ctx context.Context) context.Context {\n\t\tctx = context.WithValue(ctx, internal.KeyPath, path)\n\t\treturn context.WithValue(ctx, internal.KeyFields, fieldSel)\n\t})\n\tv.SetLabelSelector(labelSel, true)\n\n\tns, _ := client.Namespaced(path)\n\tif err := app.Config.SetActiveNamespace(ns); err != nil {\n\t\tslog.Error(\"Unable to set active namespace during show replicasets\", slogs.Error, err)\n\t}\n\tif err := app.inject(v, false); err != nil {\n\t\tapp.Flash().Err(err)\n\t}\n}\n\nfunc showPods(app *App, path string, labelSel labels.Selector, fieldSel string) {\n\tv := NewPod(client.PodGVR)\n\tv.SetContextFn(podCtx(app, path, fieldSel))\n\tv.SetLabelSelector(labelSel, true)\n\n\tns, _ := client.Namespaced(path)\n\tif err := app.Config.SetActiveNamespace(ns); err != nil {\n\t\tslog.Error(\"Unable to set active namespace during show pods\", slogs.Error, err)\n\t}\n\tif err := app.inject(v, false); err != nil {\n\t\tapp.Flash().Err(err)\n\t}\n}\n\nfunc podCtx(_ *App, path, fieldSel string) ContextFunc {\n\treturn func(ctx context.Context) context.Context {\n\t\tctx = context.WithValue(ctx, internal.KeyPath, path)\n\t\treturn context.WithValue(ctx, internal.KeyFields, fieldSel)\n\t}\n}\n\nfunc extractApp(ctx context.Context) (*App, error) {\n\tapp, ok := ctx.Value(internal.KeyApp).(*App)\n\tif !ok {\n\t\treturn nil, errors.New(\"no application found in context\")\n\t}\n\n\treturn app, nil\n}\n\n// AsKey maps a string representation of a key to a tcell key.\nfunc asKey(key string) (tcell.Key, error) {\n\tfor k, v := range tcell.KeyNames {\n\t\tif key == v {\n\t\t\treturn k, nil\n\t\t}\n\t}\n\n\treturn 0, fmt.Errorf(\"invalid key specified: %q\", key)\n}\n\n// FwFQN returns a fully qualified ns/name:container id.\nfunc fwFQN(po, co string) string {\n\treturn po + \"|\" + co\n}\n\nfunc isTCPPort(p string) bool {\n\treturn !strings.Contains(p, \"UDP\")\n}\n\n// ContainerID computes container ID based on ns/po/co.\nfunc containerID(path, co string) string {\n\tns, n := client.Namespaced(path)\n\tpo := strings.Split(n, \"-\")[0]\n\n\treturn ns + \"/\" + po + \":\" + co\n}\n\n// UrlFor computes fq url for a given benchmark configuration.\nfunc urlFor(cfg *config.BenchConfig, port string) string {\n\thost := \"localhost\"\n\tif cfg.HTTP.Host != \"\" {\n\t\thost = cfg.HTTP.Host\n\t}\n\n\tpath := \"/\"\n\tif cfg.HTTP.Path != \"\" {\n\t\tpath = cfg.HTTP.Path\n\t}\n\n\treturn \"http://\" + host + \":\" + port + path\n}\n\nfunc fqn(ns, n string) string {\n\tif ns == \"\" {\n\t\treturn n\n\t}\n\treturn ns + \"/\" + n\n}\n\nfunc decorateCpuMemHeaderRows(app *App, data *model1.TableData) {\n\tfor colIndex, header := range data.Header() {\n\t\tvar check string\n\t\tif header.Name == \"%CPU/L\" {\n\t\t\tcheck = config.CPU\n\t\t}\n\t\tif header.Name == \"%MEM/L\" {\n\t\t\tcheck = config.MEM\n\t\t}\n\t\tif check == \"\" {\n\t\t\tcontinue\n\t\t}\n\t\tdata.RowsRange(func(_ int, re model1.RowEvent) bool {\n\t\t\tif re.Row.Fields[colIndex] == render.NAValue {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tn, err := strconv.Atoi(re.Row.Fields[colIndex])\n\t\t\tif err != nil {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tif n > 100 {\n\t\t\t\tn = 100\n\t\t\t}\n\t\t\tseverity := app.Config.K9s.Thresholds.LevelFor(check, n)\n\t\t\tif severity == config.SeverityLow {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tcolor := app.Config.K9s.Thresholds.SeverityColor(check, n)\n\t\t\tif color != \"\" {\n\t\t\t\tre.Row.Fields[colIndex] = \"[\" + color + \"::b]\" + re.Row.Fields[colIndex]\n\t\t\t}\n\n\t\t\treturn true\n\t\t})\n\t}\n}\n\nfunc matchTag(i int, s string) string {\n\treturn `<<<\"search_` + strconv.Itoa(i) + `\">>>` + s + `<<<\"\">>>`\n}\n\nfunc linesWithRegions(lines []string, matches fuzzy.Matches) []string {\n\tll := make([]string, len(lines))\n\tcopy(ll, lines)\n\toffsetForLine := make(map[int]int)\n\tfor i, m := range matches {\n\t\tfor _, loc := range dao.ContinuousRanges(m.MatchedIndexes) {\n\t\t\tstart, end := loc[0]+offsetForLine[m.Index], loc[1]+offsetForLine[m.Index]\n\t\t\tline := ll[m.Index]\n\t\t\tif end > len(line) {\n\t\t\t\tend = len(line)\n\t\t\t}\n\t\t\tregionStr := matchTag(i, line[start:end])\n\t\t\tll[m.Index] = line[:start] + regionStr + line[end:]\n\t\t\toffsetForLine[m.Index] += len(regionStr) - (end - start)\n\t\t}\n\t}\n\treturn ll\n}\n"
  },
  {
    "path": "internal/view/helpers_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"log/slog\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/config/mock\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/sahilm/fuzzy\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"k8s.io/cli-runtime/pkg/genericclioptions\"\n)\n\nfunc init() {\n\tslog.SetDefault(slog.New(slog.DiscardHandler))\n}\n\nfunc TestParsePFAnn(t *testing.T) {\n\tuu := map[string]struct {\n\t\tann, co, port string\n\t\tok            bool\n\t}{\n\t\t\"named-port\": {\n\t\t\tann:  \"c1:blee\",\n\t\t\tco:   \"c1\",\n\t\t\tport: \"blee\",\n\t\t\tok:   true,\n\t\t},\n\t\t\"port-num\": {\n\t\t\tann:  \"c1:1234\",\n\t\t\tco:   \"c1\",\n\t\t\tport: \"1234\",\n\t\t\tok:   true,\n\t\t},\n\t\t\"toast\": {\n\t\t\tann: \"zorg\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tco, port, ok := parsePFAnn(u.ann)\n\t\t\tif u.ok {\n\t\t\t\tassert.Equal(t, u.co, co)\n\t\t\t\tassert.Equal(t, u.port, port)\n\t\t\t\tassert.Equal(t, u.ok, ok)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestExtractApp(t *testing.T) {\n\tapp := NewApp(mock.NewMockConfig(t))\n\n\tuu := map[string]struct {\n\t\tapp *App\n\t\terr error\n\t}{\n\t\t\"cool\":     {app: app},\n\t\t\"not-cool\": {err: errors.New(\"no application found in context\")},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tctx := context.Background()\n\t\tif u.app != nil {\n\t\t\tctx = context.WithValue(ctx, internal.KeyApp, u.app)\n\t\t}\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tapp, err := extractApp(ctx)\n\t\t\tassert.Equal(t, u.err, err)\n\t\t\tif err == nil {\n\t\t\t\tassert.Equal(t, u.app, app)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestFwFQN(t *testing.T) {\n\tuu := map[string]struct {\n\t\tpo, co, e string\n\t}{\n\t\t\"cool\": {po: \"p1\", co: \"c1\", e: \"p1|c1\"},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, fwFQN(u.po, u.co))\n\t\t})\n\t}\n}\n\nfunc TestAsKey(t *testing.T) {\n\tuu := map[string]struct {\n\t\tk   string\n\t\terr error\n\t\te   tcell.Key\n\t}{\n\t\t\"cool\": {k: \"Ctrl-A\", e: tcell.KeyCtrlA},\n\t\t\"miss\": {k: \"fred\", e: 0, err: errors.New(`invalid key specified: \"fred\"`)},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tkey, err := asKey(u.k)\n\t\t\tassert.Equal(t, u.err, err)\n\t\t\tassert.Equal(t, u.e, key)\n\t\t})\n\t}\n}\n\nfunc TestK8sEnv(t *testing.T) {\n\tcl, ctx, cfg, u := \"cluster1\", \"context1\", \"cfg1\", \"user1\"\n\tflags := genericclioptions.ConfigFlags{\n\t\tClusterName:  &cl,\n\t\tContext:      &ctx,\n\t\tAuthInfoName: &u,\n\t\tKubeConfig:   &cfg,\n\t}\n\tc := client.NewConfig(&flags)\n\tenv := k8sEnv(c)\n\n\tassert.Len(t, env, 5)\n\tassert.Equal(t, cl, env[\"CLUSTER\"])\n\tassert.Equal(t, ctx, env[\"CONTEXT\"])\n\tassert.Equal(t, u, env[\"USER\"])\n\tassert.Equal(t, render.NAValue, env[\"GROUPS\"])\n\tassert.Equal(t, cfg, env[\"KUBECONFIG\"])\n}\n\nfunc TestK9sEnv(t *testing.T) {\n\tcl, ctx, cfg, u := \"cluster1\", \"context1\", \"cfg1\", \"user1\"\n\tflags := genericclioptions.ConfigFlags{\n\t\tClusterName:  &cl,\n\t\tContext:      &ctx,\n\t\tAuthInfoName: &u,\n\t\tKubeConfig:   &cfg,\n\t}\n\tc := client.NewConfig(&flags)\n\th := model1.Header{\n\t\t{Name: \"A\"},\n\t\t{Name: \"B\"},\n\t\t{Name: \"C\"},\n\t}\n\tr := model1.Row{\n\t\tFields: []string{\"a1\", \"b1\", \"c1\"},\n\t}\n\tenv := defaultEnv(c, \"fred/blee\", h, &r)\n\n\tassert.Len(t, env, 10)\n\tassert.Equal(t, cl, env[\"CLUSTER\"])\n\tassert.Equal(t, ctx, env[\"CONTEXT\"])\n\tassert.Equal(t, u, env[\"USER\"])\n\tassert.Equal(t, render.NAValue, env[\"GROUPS\"])\n\tassert.Equal(t, cfg, env[\"KUBECONFIG\"])\n\tassert.Equal(t, \"fred\", env[\"NAMESPACE\"])\n\tassert.Equal(t, \"blee\", env[\"NAME\"])\n\tassert.Equal(t, \"a1\", env[\"COL-A\"])\n\tassert.Equal(t, \"b1\", env[\"COL-B\"])\n\tassert.Equal(t, \"c1\", env[\"COL-C\"])\n}\n\nfunc TestIsTCPPort(t *testing.T) {\n\tuu := map[string]struct {\n\t\tp string\n\t\te bool\n\t}{\n\t\t\"tcp\": {\"80╱TCP\", true},\n\t\t\"udp\": {\"80╱UDP\", false},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, isTCPPort(u.p))\n\t\t})\n\t}\n}\n\nfunc TestFQN(t *testing.T) {\n\tuu := map[string]struct {\n\t\tns, n, e string\n\t}{\n\t\t\"fullFQN\": {\"blee\", \"fred\", \"blee/fred\"},\n\t\t\"allNS\":   {\"\", \"fred\", \"fred\"},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, fqn(u.ns, u.n))\n\t\t})\n\t}\n}\n\nfunc TestUrlFor(t *testing.T) {\n\tuu := map[string]struct {\n\t\tcfg      config.BenchConfig\n\t\tco, port string\n\t\te        string\n\t}{\n\t\t\"empty\": {\n\t\t\tconfig.BenchConfig{}, \"c1\", \"9000\", \"http://localhost:9000/\",\n\t\t},\n\t\t\"path\": {\n\t\t\tconfig.BenchConfig{\n\t\t\t\tHTTP: config.HTTP{\n\t\t\t\t\tPath: \"/fred/blee\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"c1\",\n\t\t\t\"9000\",\n\t\t\t\"http://localhost:9000/fred/blee\",\n\t\t},\n\t\t\"host/path\": {\n\t\t\tconfig.BenchConfig{\n\t\t\t\tHTTP: config.HTTP{\n\t\t\t\t\tHost: \"zorg\",\n\t\t\t\t\tPath: \"/fred/blee\",\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"c1\",\n\t\t\t\"9000\",\n\t\t\t\"http://zorg:9000/fred/blee\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, urlFor(&u.cfg, u.port))\n\t\t})\n\t}\n}\n\nfunc TestContainerID(t *testing.T) {\n\tuu := map[string]struct {\n\t\tpath, co string\n\t\te        string\n\t}{\n\t\t\"plain\": {\n\t\t\t\"fred/blee\", \"c1\", \"fred/blee:c1\",\n\t\t},\n\t\t\"podID\": {\n\t\t\t\"fred/blee-78f8b5d78c-f8588\", \"c1\", \"fred/blee:c1\",\n\t\t},\n\t\t\"stsID\": {\n\t\t\t\"fred/blee-1\", \"c1\", \"fred/blee:c1\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, containerID(u.path, u.co))\n\t\t})\n\t}\n}\n\nfunc Test_linesWithRegions(t *testing.T) {\n\tuu := map[string]struct {\n\t\tlines   []string\n\t\tmatches fuzzy.Matches\n\t\te       []string\n\t}{\n\t\t\"empty-lines\": {\n\t\t\te: []string{},\n\t\t},\n\t\t\"no-match\": {\n\t\t\tlines: []string{\"bar\"},\n\t\t\te:     []string{\"bar\"},\n\t\t},\n\t\t\"single-match\": {\n\t\t\tlines: []string{\"foo\", \"bar\", \"baz\"},\n\t\t\tmatches: fuzzy.Matches{\n\t\t\t\t{Index: 1, MatchedIndexes: []int{0, 1, 2}},\n\t\t\t},\n\t\t\te: []string{\"foo\", matchTag(0, \"bar\"), \"baz\"},\n\t\t},\n\t\t\"single-character\": {\n\t\t\tlines: []string{\"foo\", \"bar\", \"baz\"},\n\t\t\tmatches: fuzzy.Matches{\n\t\t\t\t{Index: 1, MatchedIndexes: []int{1}},\n\t\t\t},\n\t\t\te: []string{\"foo\", \"b\" + matchTag(0, \"a\") + \"r\", \"baz\"},\n\t\t},\n\t\t\"multiple-matches\": {\n\t\t\tlines: []string{\"foo\", \"bar\", \"baz\"},\n\t\t\tmatches: fuzzy.Matches{\n\t\t\t\t{Index: 1, MatchedIndexes: []int{0, 1, 2}},\n\t\t\t\t{Index: 2, MatchedIndexes: []int{0, 1, 2}},\n\t\t\t},\n\t\t\te: []string{\"foo\", matchTag(0, \"bar\"), matchTag(1, \"baz\")},\n\t\t},\n\t\t\"multiple-matches-same-line\": {\n\t\t\tlines: []string{\"foosfoo baz\", \"dfbarfoos bar\"},\n\t\t\tmatches: fuzzy.Matches{\n\t\t\t\t{Index: 0, MatchedIndexes: []int{0, 1, 2}},\n\t\t\t\t{Index: 0, MatchedIndexes: []int{4, 5, 6}},\n\t\t\t\t{Index: 1, MatchedIndexes: []int{5, 6, 7}},\n\t\t\t},\n\t\t\te: []string{\n\t\t\t\tmatchTag(0, \"foo\") + \"s\" + matchTag(1, \"foo\") + \" baz\",\n\t\t\t\t\"dfbar\" + matchTag(2, \"foo\") + \"s bar\",\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tassert.Equal(t, u.e, linesWithRegions(u.lines, u.matches))\n\t\t})\n\t}\n}\n\nfunc Test_sanitizeEsc(t *testing.T) {\n\tuu := map[string]struct {\n\t\ts string\n\t\te string\n\t}{\n\t\t\"empty\": {},\n\t\t\"empty-brackets\": {\n\t\t\ts: \"[]\",\n\t\t\te: \"[]\",\n\t\t},\n\t\t\"tag\": {\n\t\t\ts: \"[fred[]\",\n\t\t\te: \"[fred]\",\n\t\t},\n\t}\n\n\tfor k, u := range uu {\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, sanitizeEsc(u.s))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/view/image_extender.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\tcorev1 \"k8s.io/api/core/v1\"\n)\n\nconst imageKey = \"setImage\"\n\ntype imageFormSpec struct {\n\tname, dockerImage, newDockerImage string\n\tinit                              bool\n}\n\nfunc (m *imageFormSpec) modified() bool {\n\tnewDockerImage := strings.TrimSpace(m.newDockerImage)\n\treturn newDockerImage != \"\" && m.dockerImage != newDockerImage\n}\n\nfunc (m *imageFormSpec) imageSpec() dao.ImageSpec {\n\tret := dao.ImageSpec{\n\t\tName: m.name,\n\t\tInit: m.init,\n\t}\n\n\tif m.modified() {\n\t\tret.DockerImage = strings.TrimSpace(m.newDockerImage)\n\t} else {\n\t\tret.DockerImage = m.dockerImage\n\t}\n\n\treturn ret\n}\n\n// ImageExtender provides for overriding container images.\ntype ImageExtender struct {\n\tResourceViewer\n}\n\n// NewImageExtender returns a new extender.\nfunc NewImageExtender(r ResourceViewer) ResourceViewer {\n\ts := ImageExtender{ResourceViewer: r}\n\ts.AddBindKeysFn(s.bindKeys)\n\n\treturn &s\n}\n\nfunc (s *ImageExtender) bindKeys(aa *ui.KeyActions) {\n\tif s.App().Config.IsReadOnly() {\n\t\treturn\n\t}\n\taa.Add(ui.KeyI, ui.NewKeyAction(\"Set Image\", s.setImageCmd, false))\n}\n\nfunc (s *ImageExtender) setImageCmd(*tcell.EventKey) *tcell.EventKey {\n\tpath := s.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn nil\n\t}\n\n\ts.Stop()\n\tdefer s.Start()\n\tif err := s.showImageDialog(path); err != nil {\n\t\ts.App().Flash().Err(err)\n\t}\n\n\treturn nil\n}\n\nfunc (s *ImageExtender) showImageDialog(path string) error {\n\tform, err := s.makeSetImageForm(path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tconfirm := tview.NewModalForm(\"<Set image>\", form)\n\tconfirm.SetText(fmt.Sprintf(\"Set image %s %s\", s.GVR(), path))\n\tconfirm.SetDoneFunc(func(int, string) {\n\t\ts.dismissDialog()\n\t})\n\ts.App().Content.AddPage(imageKey, confirm, false, false)\n\ts.App().Content.ShowPage(imageKey)\n\n\treturn nil\n}\n\nfunc (s *ImageExtender) makeSetImageForm(fqn string) (*tview.Form, error) {\n\tpodSpec, err := s.getPodSpec(fqn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tformContainerLines := make([]*imageFormSpec, 0, len(podSpec.InitContainers)+len(podSpec.Containers))\n\tfor i := range podSpec.InitContainers {\n\t\tspec := podSpec.InitContainers[i]\n\t\tformContainerLines = append(formContainerLines, &imageFormSpec{init: true, name: spec.Name, dockerImage: spec.Image})\n\t}\n\tfor i := range podSpec.Containers {\n\t\tspec := podSpec.Containers[i]\n\t\tformContainerLines = append(formContainerLines, &imageFormSpec{name: spec.Name, dockerImage: spec.Image})\n\t}\n\n\tstyles := s.App().Styles.Dialog()\n\tf := tview.NewForm().\n\t\tSetItemPadding(0).\n\t\tSetButtonsAlign(tview.AlignCenter).\n\t\tSetButtonBackgroundColor(styles.ButtonBgColor.Color()).\n\t\tSetButtonTextColor(styles.ButtonFgColor.Color()).\n\t\tSetLabelColor(styles.LabelFgColor.Color()).\n\t\tSetFieldTextColor(styles.FieldFgColor.Color()).\n\t\tAddButton(\"OK\", func() {\n\t\t\tdefer s.dismissDialog()\n\t\t\tvar imageSpecsModified dao.ImageSpecs\n\t\t\tfor _, v := range formContainerLines {\n\t\t\t\tif v.modified() {\n\t\t\t\t\timageSpecsModified = append(imageSpecsModified, v.imageSpec())\n\t\t\t\t}\n\t\t\t}\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), s.App().Conn().Config().CallTimeout())\n\t\t\tdefer cancel()\n\t\t\tif err := s.setImages(ctx, fqn, imageSpecsModified); err != nil {\n\t\t\t\tslog.Error(\"Unable to set image name\",\n\t\t\t\t\tslogs.FQN, fqn,\n\t\t\t\t\tslogs.Error, err,\n\t\t\t\t)\n\t\t\t\ts.App().Flash().Err(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\ts.App().Flash().Infof(\"Resource %s:%s image updated successfully\", s.GVR(), fqn)\n\t\t}).\n\t\tAddButton(\"Cancel\", func() {\n\t\t\ts.dismissDialog()\n\t\t})\n\n\tfor i := range formContainerLines {\n\t\tctn := formContainerLines[i]\n\t\tf.AddInputField(ctn.name, ctn.dockerImage, 0, nil, func(changed string) {\n\t\t\tctn.newDockerImage = changed\n\t\t})\n\t}\n\n\tfor i := range f.GetButtonCount() {\n\t\tf.GetButton(i).\n\t\t\tSetBackgroundColorActivated(styles.ButtonFocusBgColor.Color()).\n\t\t\tSetLabelColorActivated(styles.ButtonFocusFgColor.Color())\n\t}\n\n\treturn f, nil\n}\n\nfunc (s *ImageExtender) dismissDialog() {\n\ts.App().Content.RemovePage(imageKey)\n}\n\nfunc (s *ImageExtender) getPodSpec(path string) (*corev1.PodSpec, error) {\n\tres, err := dao.AccessorFor(s.App().factory, s.GVR())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tresourceWPodSpec, ok := res.(dao.ContainsPodSpec)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"expecting a ContainsPodSpec for %q but got %T\", s.GVR(), res)\n\t}\n\n\treturn resourceWPodSpec.GetPodSpec(path)\n}\n\nfunc (s *ImageExtender) setImages(ctx context.Context, path string, imageSpecs dao.ImageSpecs) error {\n\tres, err := dao.AccessorFor(s.App().factory, s.GVR())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tresourceWPodSpec, ok := res.(dao.ContainsPodSpec)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting a scalable resource for %q\", s.GVR())\n\t}\n\n\treturn resourceWPodSpec.SetImages(ctx, path, imageSpecs)\n}\n"
  },
  {
    "path": "internal/view/img_scan.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"errors\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\nconst (\n\timgScanTitle = \"Scans\"\n\tbrowseOSX    = \"open\"\n\tbrowseLinux  = \"sensible-browser\"\n\tcveGovURL    = \"https://nvd.nist.gov/vuln/detail/\"\n\tghsaURL      = \"https://github.com/advisories/\"\n)\n\n// ImageScan represents an image vulnerability scan view.\ntype ImageScan struct {\n\tResourceViewer\n}\n\n// NewImageScan returns a new scans view.\nfunc NewImageScan(gvr *client.GVR) ResourceViewer {\n\tv := ImageScan{}\n\tv.ResourceViewer = NewBrowser(gvr)\n\tv.AddBindKeysFn(v.bindKeys)\n\tv.GetTable().SetEnterFn(v.viewCVE)\n\tv.GetTable().SetSortCol(\"SEVERITY\", true)\n\n\treturn &v\n}\n\n// Name returns the component name.\nfunc (*ImageScan) Name() string { return imgScanTitle }\n\nfunc (i *ImageScan) bindKeys(aa *ui.KeyActions) {\n\taa.Delete(ui.KeyShiftA, ui.KeyShiftN, ui.KeyShiftS, tcell.KeyCtrlZ, tcell.KeyCtrlW)\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyShiftL: ui.NewKeyAction(\"Sort Lib\", i.GetTable().SortColCmd(\"LIBRARY\", false), true),\n\t\tui.KeyShiftS: ui.NewKeyAction(\"Sort Severity\", i.GetTable().SortColCmd(\"SEVERITY\", false), true),\n\t\tui.KeyShiftF: ui.NewKeyAction(\"Sort Fixed-in\", i.GetTable().SortColCmd(\"FIXED-IN\", false), true),\n\t\tui.KeyShiftV: ui.NewKeyAction(\"Sort Vulnerability\", i.GetTable().SortColCmd(\"VULNERABILITY\", false), true),\n\t})\n}\n\nfunc (*ImageScan) viewCVE(app *App, _ ui.Tabular, _ *client.GVR, path string) {\n\tbin := browseLinux\n\tif runtime.GOOS == \"darwin\" {\n\t\tbin = browseOSX\n\t}\n\n\ttt := strings.Split(path, \"|\")\n\tif len(tt) < 7 {\n\t\tapp.Flash().Errf(\"parse path failed: %s\", path)\n\t}\n\tcve := tt[render.CVEParseIdx]\n\tsite := cveGovURL\n\tif strings.Index(cve, \"GHSA\") == 0 {\n\t\tsite = ghsaURL\n\t}\n\tsite += cve\n\n\tok, errChan, _ := run(app, &shellOpts{\n\t\tbackground: true,\n\t\tbinary:     bin,\n\t\targs:       []string{site},\n\t})\n\tif !ok {\n\t\tapp.Flash().Errf(\"unable to run browser command\")\n\t\treturn\n\t}\n\tvar errs error\n\tfor e := range errChan {\n\t\terrs = errors.Join(e)\n\t}\n\tif errs != nil {\n\t\tapp.Flash().Err(errs)\n\t}\n}\n"
  },
  {
    "path": "internal/view/job.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"errors\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\tbatchv1 \"k8s.io/api/batch/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\n// Job represents a job viewer.\ntype Job struct {\n\tResourceViewer\n}\n\n// NewJob returns a new viewer.\nfunc NewJob(gvr *client.GVR) ResourceViewer {\n\tvar j Job\n\n\tj.ResourceViewer = NewVulnerabilityExtender(\n\t\tNewOwnerExtender(\n\t\t\tNewLogsExtender(NewBrowser(gvr), j.logOptions),\n\t\t),\n\t)\n\tj.GetTable().SetEnterFn(j.showPods)\n\tj.GetTable().SetSortCol(\"AGE\", true)\n\n\treturn &j\n}\n\nfunc (*Job) showPods(app *App, _ ui.Tabular, gvr *client.GVR, path string) {\n\to, err := app.factory.Get(gvr, path, true, labels.Everything())\n\tif err != nil {\n\t\tapp.Flash().Err(err)\n\t\treturn\n\t}\n\n\tvar job batchv1.Job\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &job)\n\tif err != nil {\n\t\tapp.Flash().Err(err)\n\t\treturn\n\t}\n\n\tshowPodsFromSelector(app, path, job.Spec.Selector)\n}\n\nfunc (j *Job) logOptions(prev bool) (*dao.LogOptions, error) {\n\tpath := j.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn nil, errors.New(\"you must provide a selection\")\n\t}\n\tjob, err := j.getInstance(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn podLogOptions(j.App(), path, prev, &job.ObjectMeta, &job.Spec.Template.Spec), nil\n}\n\nfunc (j *Job) getInstance(fqn string) (*batchv1.Job, error) {\n\tvar job dao.Job\n\tjob.Init(j.App().factory, client.JobGVR)\n\n\treturn job.GetInstance(fqn)\n}\n"
  },
  {
    "path": "internal/view/live_view.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\t\"github.com/sahilm/fuzzy\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n)\n\nconst (\n\tliveViewTitleFmt = \"[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] \"\n\tyamlAction       = \"YAML\"\n)\n\n// LiveView represents a live text viewer.\ntype LiveView struct {\n\t*tview.Flex\n\n\ttitle                     string\n\tmodel                     model.ResourceViewer\n\ttext                      *tview.TextView\n\tactions                   *ui.KeyActions\n\tapp                       *App\n\tcmdBuff                   *model.FishBuff\n\tcurrentRegion, maxRegions int\n\tcancel                    context.CancelFunc\n\tfullScreen                bool\n\tmanagedField              bool\n\tautoRefresh               bool\n}\n\n// NewLiveView returns a live viewer.\nfunc NewLiveView(app *App, title string, m model.ResourceViewer) *LiveView {\n\tv := LiveView{\n\t\tFlex:          tview.NewFlex(),\n\t\ttext:          tview.NewTextView(),\n\t\tapp:           app,\n\t\ttitle:         title,\n\t\tactions:       ui.NewKeyActions(),\n\t\tcurrentRegion: 0,\n\t\tmaxRegions:    0,\n\t\tcmdBuff:       model.NewFishBuff('/', model.FilterBuffer),\n\t\tmodel:         m,\n\t\tautoRefresh:   app.Config.K9s.LiveViewAutoRefresh,\n\t}\n\tv.AddItem(v.text, 0, 1, true)\n\n\treturn &v\n}\n\nfunc (*LiveView) SetCommand(*cmd.Interpreter)            {}\nfunc (*LiveView) SetFilter(string, bool)                 {}\nfunc (*LiveView) SetLabelSelector(labels.Selector, bool) {}\n\n// Init initializes the viewer.\nfunc (v *LiveView) Init(_ context.Context) error {\n\tif v.title != \"\" {\n\t\tv.SetBorder(true)\n\t}\n\tv.text.SetScrollable(true).SetWrap(true).SetRegions(true)\n\tv.text.SetDynamicColors(true)\n\tv.text.SetHighlightColor(tcell.ColorOrange)\n\tv.SetTitleColor(tcell.ColorAqua)\n\tv.SetInputCapture(v.keyboard)\n\tv.SetBorderPadding(0, 0, 1, 1)\n\tv.updateTitle()\n\n\tv.app.Styles.AddListener(v)\n\tv.StylesChanged(v.app.Styles)\n\tv.setFullScreen(v.app.Config.K9s.UI.DefaultsToFullScreen)\n\n\tv.app.Prompt().SetModel(v.cmdBuff)\n\tv.cmdBuff.AddListener(v)\n\n\tv.bindKeys()\n\tv.SetInputCapture(v.keyboard)\n\tif v.model != nil {\n\t\tv.model.AddListener(v)\n\t}\n\n\treturn nil\n}\n\n// InCmdMode checks if prompt is active.\nfunc (v *LiveView) InCmdMode() bool {\n\treturn v.cmdBuff.InCmdMode()\n}\n\n// ResourceFailed notifies when there is an issue.\nfunc (v *LiveView) ResourceFailed(err error) {\n\tv.text.SetTextAlign(tview.AlignCenter)\n\tx, _, w, _ := v.GetRect()\n\tv.text.SetText(cowTalk(err.Error(), x+w))\n}\n\n// ResourceChanged notifies when the filter changes.\nfunc (v *LiveView) ResourceChanged(lines []string, matches fuzzy.Matches) {\n\tv.app.QueueUpdateDraw(func() {\n\t\tv.text.SetTextAlign(tview.AlignLeft)\n\t\tv.currentRegion, v.maxRegions = 0, len(matches)\n\n\t\tif v.text.GetText(true) == \"\" {\n\t\t\tv.text.ScrollToBeginning()\n\t\t}\n\n\t\tlines = linesWithRegions(lines, matches)\n\t\tv.text.SetText(colorizeYAML(v.app.Styles.Views().Yaml, strings.Join(lines, \"\\n\")))\n\t\tv.text.Highlight()\n\t\tif v.currentRegion < v.maxRegions {\n\t\t\tv.text.Highlight(\"search_\" + strconv.Itoa(v.currentRegion))\n\t\t\tv.text.ScrollToHighlight()\n\t\t}\n\t\tv.updateTitle()\n\t})\n}\n\n// BufferChanged indicates the buffer was changed.\nfunc (*LiveView) BufferChanged(_, _ string) {}\n\n// BufferCompleted indicates input was accepted.\nfunc (v *LiveView) BufferCompleted(text, _ string) {\n\tv.model.Filter(text)\n}\n\n// BufferActive indicates the buff activity changed.\nfunc (v *LiveView) BufferActive(state bool, k model.BufferKind) {\n\tv.app.BufferActive(state, k)\n}\n\nfunc (v *LiveView) bindKeys() {\n\tv.actions.Bulk(ui.KeyMap{\n\t\ttcell.KeyEnter:  ui.NewSharedKeyAction(\"Filter\", v.filterCmd, false),\n\t\ttcell.KeyEscape: ui.NewKeyAction(\"Back\", v.resetCmd, false),\n\t\tui.KeyQ:         ui.NewKeyAction(\"Back\", v.resetCmd, false),\n\t\ttcell.KeyCtrlS:  ui.NewKeyAction(\"Save\", v.saveCmd, false),\n\t\tui.KeyC:         ui.NewKeyAction(\"Copy\", cpCmd(v.app.Flash(), v.text), true),\n\t\tui.KeyF:         ui.NewKeyAction(\"Toggle FullScreen\", v.toggleFullScreenCmd, true),\n\t\tui.KeyR:         ui.NewKeyAction(\"Toggle Auto-Refresh\", v.toggleRefreshCmd, true),\n\t\tui.KeyN:         ui.NewKeyAction(\"Next Match\", v.nextCmd, true),\n\t\tui.KeyShiftN:    ui.NewKeyAction(\"Prev Match\", v.prevCmd, true),\n\t\tui.KeySlash:     ui.NewSharedKeyAction(\"Filter Mode\", v.activateCmd, false),\n\t\ttcell.KeyDelete: ui.NewSharedKeyAction(\"Erase\", v.eraseCmd, false),\n\t})\n\n\tif !v.app.Config.IsReadOnly() {\n\t\tv.actions.Add(ui.KeyE, ui.NewKeyAction(\"Edit\", v.editCmd, true))\n\t}\n\tif v.title == yamlAction {\n\t\tv.actions.Add(ui.KeyM, ui.NewKeyAction(\"Toggle ManagedFields\", v.toggleManagedCmd, true))\n\t}\n\tif _, ok := v.model.(model.EncDecResourceViewer); ok {\n\t\tv.actions.Add(ui.KeyX, ui.NewKeyAction(\"Toggle Decode\", v.toggleEncodedDecodedCmd, true))\n\t}\n}\n\nfunc (v *LiveView) toggleEncodedDecodedCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tm, ok := v.model.(model.EncDecResourceViewer)\n\tif !ok {\n\t\treturn evt\n\t}\n\n\tm.Toggle()\n\tv.Start()\n\treturn nil\n}\n\nfunc (v *LiveView) editCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := v.model.GetPath()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\tv.Stop()\n\tdefer v.Start()\n\tif err := editRes(v.app, v.model.GVR(), path); err != nil {\n\t\tv.app.Flash().Err(err)\n\t}\n\n\treturn nil\n}\n\n// ToggleRefreshCmd is used for pausing the refreshing of data on config map and secrets.\nfunc (v *LiveView) toggleRefreshCmd(*tcell.EventKey) *tcell.EventKey {\n\tv.autoRefresh = !v.autoRefresh\n\tif v.autoRefresh {\n\t\tv.Start()\n\t\tv.app.Flash().Info(\"Auto-refresh is enabled\")\n\t\treturn nil\n\t}\n\tv.Stop()\n\tv.app.Flash().Info(\"Auto-refresh is disabled\")\n\n\treturn nil\n}\n\nfunc (v *LiveView) keyboard(evt *tcell.EventKey) *tcell.EventKey {\n\tif a, ok := v.actions.Get(ui.AsKey(evt)); ok {\n\t\treturn a.Action(evt)\n\t}\n\n\treturn evt\n}\n\n// StylesChanged notifies the skin changed.\nfunc (v *LiveView) StylesChanged(s *config.Styles) {\n\tv.SetBackgroundColor(s.BgColor())\n\tv.text.SetTextColor(s.FgColor())\n\tv.SetBorderFocusColor(s.Frame().Border.FocusColor.Color())\n}\n\n// Actions returns menu actions.\nfunc (v *LiveView) Actions() *ui.KeyActions {\n\treturn v.actions\n}\n\n// Name returns the component name.\nfunc (v *LiveView) Name() string { return v.title }\n\n// Start starts the view updater.\nfunc (v *LiveView) Start() {\n\tif v.autoRefresh {\n\t\tvar ctx context.Context\n\t\tctx, v.cancel = context.WithCancel(v.defaultCtx())\n\n\t\tif err := v.model.Watch(ctx); err != nil {\n\t\t\tslog.Error(\"LiveView watcher failed\", slogs.Error, err)\n\t\t}\n\t\treturn\n\t}\n\tif err := v.model.Refresh(v.defaultCtx()); err != nil {\n\t\tslog.Error(\"LiveView refresh failed\", slogs.Error, err)\n\t}\n}\n\nfunc (v *LiveView) defaultCtx() context.Context {\n\treturn context.WithValue(context.Background(), internal.KeyFactory, v.app.factory)\n}\n\n// Stop terminates the updater.\nfunc (v *LiveView) Stop() {\n\tif v.cancel != nil {\n\t\tv.cancel()\n\t\tv.cancel = nil\n\t}\n\tv.app.Styles.RemoveListener(v)\n}\n\n// Hints returns menu hints.\nfunc (v *LiveView) Hints() model.MenuHints {\n\treturn v.actions.Hints()\n}\n\n// ExtraHints returns additional hints.\nfunc (*LiveView) ExtraHints() map[string]string {\n\treturn nil\n}\n\nfunc (v *LiveView) toggleManagedCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif v.app.InCmdMode() {\n\t\treturn evt\n\t}\n\n\tv.managedField = !v.managedField\n\tv.model.SetOptions(v.defaultCtx(), map[string]bool{model.ManagedFieldsOpts: v.managedField})\n\n\tv.app.Flash().Info(\"toggled managed fields\")\n\treturn nil\n}\n\nfunc (v *LiveView) toggleFullScreenCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif v.app.InCmdMode() {\n\t\treturn evt\n\t}\n\n\tv.setFullScreen(!v.fullScreen)\n\n\treturn nil\n}\n\nfunc (v *LiveView) setFullScreen(isFullScreen bool) {\n\tv.fullScreen = isFullScreen\n\tv.SetFullScreen(isFullScreen)\n\tv.SetBorder(!isFullScreen)\n\tif isFullScreen {\n\t\tv.SetBorderPadding(0, 0, 0, 0)\n\t} else {\n\t\tv.SetBorderPadding(0, 0, 1, 1)\n\t}\n}\n\nfunc (v *LiveView) nextCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif v.cmdBuff.Empty() {\n\t\treturn evt\n\t}\n\n\tv.currentRegion++\n\tif v.currentRegion >= v.maxRegions {\n\t\tv.currentRegion = 0\n\t}\n\tv.text.Highlight(\"search_\" + strconv.Itoa(v.currentRegion))\n\tv.text.ScrollToHighlight()\n\tv.updateTitle()\n\n\treturn nil\n}\n\nfunc (v *LiveView) prevCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif v.cmdBuff.Empty() {\n\t\treturn evt\n\t}\n\n\tv.currentRegion--\n\tif v.currentRegion < 0 {\n\t\tv.currentRegion = v.maxRegions - 1\n\t}\n\tv.text.Highlight(\"search_\" + strconv.Itoa(v.currentRegion))\n\tv.text.ScrollToHighlight()\n\tv.updateTitle()\n\n\treturn nil\n}\n\nfunc (v *LiveView) filterCmd(*tcell.EventKey) *tcell.EventKey {\n\tv.model.Filter(v.cmdBuff.GetText())\n\tv.cmdBuff.SetActive(false)\n\tv.updateTitle()\n\n\treturn nil\n}\n\nfunc (v *LiveView) activateCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif v.app.InCmdMode() {\n\t\treturn evt\n\t}\n\tv.app.ResetPrompt(v.cmdBuff)\n\n\treturn nil\n}\n\nfunc (v *LiveView) eraseCmd(*tcell.EventKey) *tcell.EventKey {\n\tif !v.cmdBuff.IsActive() {\n\t\treturn nil\n\t}\n\tv.cmdBuff.Delete()\n\n\treturn nil\n}\n\nfunc (v *LiveView) resetCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif !v.cmdBuff.InCmdMode() {\n\t\tv.cmdBuff.Reset()\n\t\treturn v.app.PrevCmd(evt)\n\t}\n\n\tif v.cmdBuff.GetText() != \"\" {\n\t\tv.model.ClearFilter()\n\t}\n\tv.cmdBuff.SetActive(false)\n\tv.cmdBuff.Reset()\n\tv.updateTitle()\n\n\treturn nil\n}\n\nfunc (v *LiveView) saveCmd(*tcell.EventKey) *tcell.EventKey {\n\tname := fmt.Sprintf(\"%s--%s\", strings.Replace(v.model.GetPath(), \"/\", \"-\", 1), strings.ToLower(v.title))\n\tif _, err := saveYAML(v.app.Config.K9s.ContextScreenDumpDir(), name, sanitizeEsc(v.text.GetText(true))); err != nil {\n\t\tv.app.Flash().Err(err)\n\t} else {\n\t\tv.app.Flash().Infof(\"File %q saved successfully!\", name)\n\t}\n\n\treturn nil\n}\n\nfunc (v *LiveView) updateTitle() {\n\tif v.title == \"\" {\n\t\treturn\n\t}\n\tvar fmat string\n\tif v.model != nil {\n\t\tfmat = fmt.Sprintf(liveViewTitleFmt, v.title, v.model.GetPath())\n\t}\n\n\tvar (\n\t\tbuff   = v.cmdBuff.GetText()\n\t\tstyles = v.app.Styles.Frame()\n\t)\n\tif buff == \"\" {\n\t\tv.SetTitle(ui.SkinTitle(fmat, &styles))\n\t\treturn\n\t}\n\n\tif v.maxRegions > 0 {\n\t\tbuff += fmt.Sprintf(\"[%d:%d]\", v.currentRegion+1, v.maxRegions)\n\t}\n\tfmat += fmt.Sprintf(ui.SearchFmt, buff)\n\tv.SetTitle(ui.SkinTitle(fmat, &styles))\n}\n"
  },
  {
    "path": "internal/view/live_view_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/config/mock\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestLiveViewSetText(t *testing.T) {\n\ts := `\napiVersion: v1\n  data:\n    the secret name you want to quote to use tls.\",\"title\":\"secretName\",\"type\":\"string\"}},\"required\":[\"http\",\"class\",\"classInSpec\"],\"type\":\"object\"}\n`\n\n\tv := NewLiveView(NewApp(mock.NewMockConfig(t)), \"fred\", nil)\n\trequire.NoError(t, v.Init(context.Background()))\n\tv.text.SetText(colorizeYAML(config.Yaml{}, s))\n\n\tassert.Equal(t, s, sanitizeEsc(v.text.GetText(true)))\n}\n"
  },
  {
    "path": "internal/view/log.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/color\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n)\n\nconst (\n\tlogTitle            = \"logs\"\n\tlogMessage          = \"Waiting for logs...\\n\"\n\tlogFmt              = \"([hilite:bg:]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] \"\n\tlogCoFmt            = \"([hilite:bg:]%s:[hilite:bg:b]%s[-:bg:-])[[green:bg:b]%s[-:bg:-]] \"\n\tdefaultFlushTimeout = 50 * time.Millisecond\n)\n\n// Log represents a generic log viewer.\ntype Log struct {\n\t*tview.Flex\n\n\tapp               *App\n\tlogs              *Logger\n\tindicator         *LogIndicator\n\tansiWriter        io.Writer\n\tmodel             *model.Log\n\tcancelFn          context.CancelFunc\n\tcancelUpdates     bool\n\tmx                sync.Mutex\n\tfollow            bool\n\tcolumnLock        bool\n\trequestOneRefresh bool\n}\n\nvar _ model.Component = (*Log)(nil)\n\n// NewLog returns a new viewer.\nfunc NewLog(gvr *client.GVR, opts *dao.LogOptions) *Log {\n\treturn &Log{\n\t\tFlex:  tview.NewFlex(),\n\t\tmodel: model.NewLog(gvr, opts, defaultFlushTimeout),\n\t}\n}\n\nfunc (*Log) SetCommand(*cmd.Interpreter)            {}\nfunc (*Log) SetFilter(string, bool)                 {}\nfunc (*Log) SetLabelSelector(labels.Selector, bool) {}\n\n// Init initializes the viewer.\nfunc (l *Log) Init(ctx context.Context) (err error) {\n\tif l.app, err = extractApp(ctx); err != nil {\n\t\treturn err\n\t}\n\tl.model.Configure(l.app.Config.K9s.Logger)\n\n\tl.SetBorder(true)\n\tl.SetDirection(tview.FlexRow)\n\n\tl.indicator = NewLogIndicator(l.app.Config, l.app.Styles, l.isContainerLogView())\n\tl.AddItem(l.indicator, 1, 1, false)\n\tif !l.model.HasDefaultContainer() {\n\t\tl.indicator.ToggleAllContainers()\n\t}\n\tl.indicator.Refresh()\n\n\tl.logs = NewLogger(l.app)\n\tif e := l.logs.Init(ctx); e != nil {\n\t\treturn e\n\t}\n\tl.logs.SetBorderPadding(0, 0, 1, 1)\n\tl.logs.SetText(\"[orange::d]\" + logMessage)\n\tl.logs.SetWrap(l.app.Config.K9s.Logger.TextWrap)\n\tl.logs.SetMaxLines(l.app.Config.K9s.Logger.BufferSize)\n\n\tl.ansiWriter = tview.ANSIWriter(l.logs, l.app.Styles.Views().Log.FgColor.String(), l.app.Styles.Views().Log.BgColor.String())\n\tl.AddItem(l.logs, 0, 1, true)\n\tl.bindKeys()\n\n\tl.StylesChanged(l.app.Styles)\n\tl.toggleFullScreen()\n\n\tl.model.Init(l.app.factory)\n\tl.updateTitle()\n\n\tl.follow = !l.app.Config.K9s.Logger.DisableAutoscroll\n\tl.columnLock = l.app.Config.K9s.Logger.ColumnLock\n\n\tl.model.ToggleShowTimestamp(l.app.Config.K9s.Logger.ShowTime)\n\n\treturn nil\n}\n\n// InCmdMode checks if prompt is active.\nfunc (l *Log) InCmdMode() bool {\n\treturn l.logs.cmdBuff.InCmdMode()\n}\n\n// LogCanceled indicates no more logs are coming.\nfunc (l *Log) LogCanceled() {\n\tslog.Debug(\"Logs watcher canceled!\")\n\tl.Flush([][]byte{[]byte(\"\\n🏁 [red::b]Stream exited! No more logs...\")})\n}\n\n// LogStop disables log flushes.\nfunc (l *Log) LogStop() {\n\tslog.Debug(\"Logs watcher stopped!\")\n\tl.mx.Lock()\n\tdefer l.mx.Unlock()\n\n\tl.cancelUpdates = true\n}\n\n// LogResume resume log flushes.\nfunc (l *Log) LogResume() {\n\tl.mx.Lock()\n\tdefer l.mx.Unlock()\n\n\tl.cancelUpdates = false\n}\n\n// LogCleared clears the logs.\nfunc (l *Log) LogCleared() {\n\tl.app.QueueUpdateDraw(func() {\n\t\tl.logs.Clear()\n\t})\n}\n\n// LogFailed notifies an error occurred.\nfunc (l *Log) LogFailed(err error) {\n\tl.app.QueueUpdateDraw(func() {\n\t\tl.app.Flash().Err(err)\n\t\tif l.logs.GetText(true) == logMessage {\n\t\t\tl.logs.Clear()\n\t\t}\n\t\tif _, err = l.ansiWriter.Write([]byte(tview.Escape(color.Colorize(err.Error(), color.Red)))); err != nil {\n\t\t\tslog.Error(\"Log line write failed\", slogs.Error, err)\n\t\t}\n\t})\n}\n\n// LogChanged updates the logs.\nfunc (l *Log) LogChanged(lines [][]byte) {\n\tl.app.QueueUpdateDraw(func() {\n\t\tif l.logs.GetText(true) == logMessage {\n\t\t\tl.logs.Clear()\n\t\t}\n\t\tl.Flush(lines)\n\t})\n}\n\n// BufferCompleted indicates input was accepted.\nfunc (l *Log) BufferCompleted(text, _ string) {\n\tl.model.Filter(text)\n\tl.updateTitle()\n}\n\n// BufferChanged indicates the buffer was changed.\nfunc (*Log) BufferChanged(_, _ string) {}\n\n// BufferActive indicates the buff activity changed.\nfunc (l *Log) BufferActive(state bool, k model.BufferKind) {\n\tl.app.BufferActive(state, k)\n}\n\n// StylesChanged reports skin changes.\nfunc (l *Log) StylesChanged(s *config.Styles) {\n\tl.SetBackgroundColor(s.Views().Log.BgColor.Color())\n\tl.logs.SetTextColor(s.Views().Log.FgColor.Color())\n\tl.logs.SetBackgroundColor(s.Views().Log.BgColor.Color())\n}\n\n// GetModel returns the log model.\nfunc (l *Log) GetModel() *model.Log {\n\treturn l.model\n}\n\n// Hints returns a collection of menu hints.\nfunc (l *Log) Hints() model.MenuHints {\n\treturn l.logs.Actions().Hints()\n}\n\n// ExtraHints returns additional hints.\nfunc (*Log) ExtraHints() map[string]string {\n\treturn nil\n}\n\nfunc (l *Log) cancel() {\n\tl.mx.Lock()\n\tdefer l.mx.Unlock()\n\tif l.cancelFn != nil {\n\t\tl.cancelFn()\n\t\tl.cancelFn = nil\n\t}\n}\n\nfunc (l *Log) getContext() context.Context {\n\tl.cancel()\n\tctx := context.Background()\n\tctx, l.cancelFn = context.WithCancel(ctx)\n\n\treturn ctx\n}\n\n// Start runs the component.\nfunc (l *Log) Start() {\n\tl.model.Start(l.getContext())\n\tl.model.AddListener(l)\n\tl.app.Styles.AddListener(l)\n\tl.logs.cmdBuff.AddListener(l)\n\tl.logs.cmdBuff.AddListener(l.app.Prompt())\n\tl.updateTitle()\n}\n\n// Stop terminates the component.\nfunc (l *Log) Stop() {\n\tl.model.RemoveListener(l)\n\tl.model.Stop()\n\tl.cancel()\n\tl.app.Styles.RemoveListener(l)\n\tl.logs.cmdBuff.RemoveListener(l)\n\tl.logs.cmdBuff.RemoveListener(l.app.Prompt())\n}\n\n// Name returns the component name.\nfunc (*Log) Name() string { return logTitle }\n\nfunc (l *Log) bindKeys() {\n\tl.logs.Actions().Bulk(ui.KeyMap{\n\t\tui.Key0:         ui.NewKeyAction(\"tail\", l.sinceCmd(-1), true),\n\t\tui.Key1:         ui.NewKeyAction(\"head\", l.sinceCmd(0), true),\n\t\tui.Key2:         ui.NewKeyAction(\"1m\", l.sinceCmd(60), true),\n\t\tui.Key3:         ui.NewKeyAction(\"5m\", l.sinceCmd(5*60), true),\n\t\tui.Key4:         ui.NewKeyAction(\"15m\", l.sinceCmd(15*60), true),\n\t\tui.Key5:         ui.NewKeyAction(\"30m\", l.sinceCmd(30*60), true),\n\t\tui.Key6:         ui.NewKeyAction(\"1h\", l.sinceCmd(60*60), true),\n\t\ttcell.KeyEnter:  ui.NewSharedKeyAction(\"Filter\", l.filterCmd, false),\n\t\ttcell.KeyEscape: ui.NewKeyAction(\"Back\", l.resetCmd, false),\n\t\tui.KeyQ:         ui.NewKeyAction(\"Back\", l.resetCmd, false),\n\t\tui.KeyShiftC:    ui.NewKeyAction(\"Clear\", l.clearCmd, true),\n\t\tui.KeyM:         ui.NewKeyAction(\"Mark\", l.markCmd, true),\n\t\tui.KeyS:         ui.NewKeyAction(\"Toggle AutoScroll\", l.toggleAutoScrollCmd, true),\n\t\tui.KeyShiftL:    ui.NewKeyAction(\"Toggle ColumnLock\", l.toggleColumnLockCmd, true),\n\t\tui.KeyF:         ui.NewKeyAction(\"Toggle FullScreen\", l.toggleFullScreenCmd, true),\n\t\tui.KeyT:         ui.NewKeyAction(\"Toggle Timestamp\", l.toggleTimestampCmd, true),\n\t\tui.KeyW:         ui.NewKeyAction(\"Toggle Wrap\", l.toggleTextWrapCmd, true),\n\t\ttcell.KeyCtrlS:  ui.NewKeyAction(\"Save\", l.SaveCmd, true),\n\t\tui.KeyC:         ui.NewKeyAction(\"Copy\", cpCmd(l.app.Flash(), l.logs.TextView), true),\n\t})\n\tif l.model.HasDefaultContainer() {\n\t\tl.logs.Actions().Add(ui.KeyA, ui.NewKeyAction(\"Toggle AllContainers\", l.toggleAllContainers, true))\n\t}\n}\n\nfunc (l *Log) resetCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif !l.logs.cmdBuff.IsActive() {\n\t\tif l.logs.cmdBuff.GetText() == \"\" {\n\t\t\treturn l.app.PrevCmd(evt)\n\t\t}\n\t}\n\n\tl.logs.cmdBuff.Reset()\n\tl.logs.cmdBuff.SetActive(false)\n\tl.model.Filter(l.logs.cmdBuff.GetText())\n\tl.updateTitle()\n\n\treturn nil\n}\n\n// SendStrokes (testing only!)\nfunc (l *Log) SendStrokes(s string) {\n\tl.app.Prompt().SendStrokes(s)\n}\n\n// SendKeys (testing only!)\nfunc (l *Log) SendKeys(kk ...tcell.Key) {\n\tfor _, k := range kk {\n\t\tl.logs.keyboard(tcell.NewEventKey(k, ' ', tcell.ModNone))\n\t}\n}\n\n// Indicator returns the scroll mode viewer.\nfunc (l *Log) Indicator() *LogIndicator {\n\treturn l.indicator\n}\n\nfunc (l *Log) updateTitle() {\n\tsinceSeconds, since := l.model.SinceSeconds(), \"tail\"\n\tif sinceSeconds > 0 && sinceSeconds < 60*60 {\n\t\tsince = fmt.Sprintf(\"%dm\", sinceSeconds/60)\n\t}\n\tif sinceSeconds >= 60*60 {\n\t\tsince = fmt.Sprintf(\"%dh\", sinceSeconds/(60*60))\n\t}\n\tif l.model.IsHead() {\n\t\tsince = \"head\"\n\t}\n\n\ttitle := \" Logs\"\n\tif l.model.LogOptions().Previous {\n\t\ttitle = \" Previous Logs\"\n\t}\n\tvar (\n\t\tpath, co = l.model.GetPath(), l.model.GetContainer()\n\t\tstyles   = l.app.Styles.Frame()\n\t)\n\tif co == \"\" {\n\t\ttitle += ui.SkinTitle(fmt.Sprintf(logFmt, path, since), &styles)\n\t} else {\n\t\ttitle += ui.SkinTitle(fmt.Sprintf(logCoFmt, path, co, since), &styles)\n\t}\n\n\tbuff := l.logs.cmdBuff.GetText()\n\tif buff != \"\" {\n\t\ttitle += ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), &styles)\n\t}\n\tl.SetTitle(title)\n}\n\n// Logs returns the log viewer.\nfunc (l *Log) Logs() *Logger {\n\treturn l.logs\n}\n\n// EOL tracks end of lines.\nvar EOL = []byte{'\\n'}\n\n// Flush write logs to viewer.\nfunc (l *Log) Flush(lines [][]byte) {\n\tdefer func() {\n\t\tif l.cancelUpdates {\n\t\t\tl.cancelUpdates = false\n\t\t}\n\t}()\n\n\tif len(lines) == 0 || (!l.requestOneRefresh && !l.indicator.AutoScroll()) || l.cancelUpdates {\n\t\treturn\n\t}\n\tif l.requestOneRefresh {\n\t\tl.requestOneRefresh = false\n\t}\n\tfor i := range lines {\n\t\tif l.cancelUpdates {\n\t\t\tbreak\n\t\t}\n\t\t_, _ = l.ansiWriter.Write(lines[i])\n\t}\n\tif l.follow {\n\t\tif l.columnLock {\n\t\t\t// Enables end tracking without resetting column\n\t\t\tl.logs.SetScrollable(false).SetScrollable(true)\n\t\t} else {\n\t\t\tl.logs.ScrollToEnd()\n\t\t}\n\t}\n}\n\n// ----------------------------------------------------------------------------\n// Actions...\n\nfunc (l *Log) sinceCmd(n int) func(evt *tcell.EventKey) *tcell.EventKey {\n\treturn func(*tcell.EventKey) *tcell.EventKey {\n\t\tl.logs.Clear()\n\t\tctx := l.getContext()\n\t\tif n == 0 {\n\t\t\tl.model.Head(ctx)\n\t\t} else {\n\t\t\tl.model.SetSinceSeconds(ctx, int64(n))\n\t\t}\n\t\tl.requestOneRefresh = true\n\t\tl.updateTitle()\n\n\t\treturn nil\n\t}\n}\n\nfunc (l *Log) toggleAllContainers(evt *tcell.EventKey) *tcell.EventKey {\n\tif l.app.InCmdMode() {\n\t\treturn evt\n\t}\n\tl.indicator.ToggleAllContainers()\n\tl.model.ToggleAllContainers(l.getContext())\n\tl.updateTitle()\n\n\treturn nil\n}\n\nfunc (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif !l.logs.cmdBuff.IsActive() {\n\t\t_, _ = fmt.Fprintln(l.ansiWriter)\n\t\treturn evt\n\t}\n\n\tl.logs.cmdBuff.SetActive(false)\n\tl.model.Filter(l.logs.cmdBuff.GetText())\n\tl.updateTitle()\n\n\treturn nil\n}\n\n// SaveCmd dumps the logs to file.\nfunc (l *Log) SaveCmd(*tcell.EventKey) *tcell.EventKey {\n\tpath, err := saveData(l.app.Config.K9s.ContextScreenDumpDir(), l.model.GetPath(), l.logs.GetText(true))\n\tif err != nil {\n\t\tl.app.Flash().Err(err)\n\t\treturn nil\n\t}\n\tl.app.Flash().Infof(\"Log %s saved successfully!\", path)\n\n\treturn nil\n}\n\nfunc ensureDir(dir string) error {\n\treturn os.MkdirAll(dir, 0744)\n}\n\nfunc saveData(dir, fqn, logs string) (string, error) {\n\tif err := ensureDir(dir); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tf := fmt.Sprintf(\"%s-%d.log\", fqn, time.Now().UnixNano())\n\tpath := filepath.Join(dir, data.SanitizeFileName(f))\n\tmod := os.O_CREATE | os.O_WRONLY\n\tfile, err := os.OpenFile(path, mod, 0600)\n\tif err != nil {\n\t\tslog.Error(\"Unable to save log file\",\n\t\t\tslogs.Path, path,\n\t\t\tslogs.Error, err,\n\t\t)\n\t\treturn \"\", nil\n\t}\n\tdefer func() {\n\t\tif err := file.Close(); err != nil {\n\t\t\tslog.Error(\"Closing Log file failed\",\n\t\t\t\tslogs.Path, path,\n\t\t\t\tslogs.Error, err,\n\t\t\t)\n\t\t}\n\t}()\n\tif _, err := file.WriteString(logs); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn path, nil\n}\n\nfunc (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey {\n\tl.model.Clear()\n\treturn nil\n}\n\nfunc (l *Log) markCmd(*tcell.EventKey) *tcell.EventKey {\n\t_, _, w, _ := l.GetRect()\n\t_, _ = fmt.Fprintf(l.ansiWriter, \"[%s:-:b]%s[-:-:-]\\n\", l.app.Styles.Views().Log.FgColor.String(), strings.Repeat(\"-\", w-4))\n\tl.follow = true\n\n\treturn nil\n}\n\nfunc (l *Log) toggleTimestampCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif l.app.InCmdMode() {\n\t\treturn evt\n\t}\n\n\tl.indicator.ToggleTimestamp()\n\tl.model.ToggleShowTimestamp(l.indicator.showTime)\n\tl.indicator.Refresh()\n\n\treturn nil\n}\n\nfunc (l *Log) toggleTextWrapCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif l.app.InCmdMode() {\n\t\treturn evt\n\t}\n\n\tl.indicator.ToggleTextWrap()\n\tl.logs.SetWrap(l.indicator.textWrap)\n\tl.indicator.Refresh()\n\n\treturn nil\n}\n\n// ToggleAutoScrollCmd toggles autoscroll status.\nfunc (l *Log) toggleAutoScrollCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif l.app.InCmdMode() {\n\t\treturn evt\n\t}\n\n\tl.indicator.ToggleAutoScroll()\n\tl.follow = l.indicator.AutoScroll()\n\tl.indicator.Refresh()\n\n\treturn nil\n}\n\nfunc (l *Log) toggleColumnLockCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif l.app.InCmdMode() {\n\t\treturn evt\n\t}\n\n\tl.indicator.ToggleColumnLock()\n\tl.columnLock = l.indicator.ColumnLock()\n\tl.indicator.Refresh()\n\n\treturn nil\n}\n\nfunc (l *Log) toggleFullScreenCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif l.app.InCmdMode() {\n\t\treturn evt\n\t}\n\tl.indicator.ToggleFullScreen()\n\tl.toggleFullScreen()\n\tl.indicator.Refresh()\n\n\treturn nil\n}\n\nfunc (l *Log) toggleFullScreen() {\n\tl.SetFullScreen(l.indicator.FullScreen())\n\tl.SetBorder(!l.indicator.FullScreen())\n\tif l.indicator.FullScreen() {\n\t\tl.logs.SetBorderPadding(0, 0, 0, 0)\n\t} else {\n\t\tl.logs.SetBorderPadding(0, 0, 1, 1)\n\t}\n}\n\nfunc (l *Log) isContainerLogView() bool {\n\treturn l.model.HasDefaultContainer()\n}\n"
  },
  {
    "path": "internal/view/log_indicator.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"fmt\"\n\t\"sync/atomic\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/tview\"\n)\n\nconst spacer = \"     \"\n\n// LogIndicator represents a log view indicator.\ntype LogIndicator struct {\n\t*tview.TextView\n\n\tstyles                     *config.Styles\n\tscrollStatus               int32\n\tindicator                  []byte\n\tfullScreen                 bool\n\ttextWrap                   bool\n\tshowTime                   bool\n\tallContainers              bool\n\tshouldDisplayAllContainers bool\n\tcolumnLock                 bool\n}\n\n// NewLogIndicator returns a new indicator.\nfunc NewLogIndicator(cfg *config.Config, styles *config.Styles, allContainers bool) *LogIndicator {\n\tl := LogIndicator{\n\t\tstyles:                     styles,\n\t\tTextView:                   tview.NewTextView(),\n\t\tindicator:                  make([]byte, 0, 100),\n\t\tscrollStatus:               1,\n\t\tfullScreen:                 cfg.K9s.UI.DefaultsToFullScreen,\n\t\ttextWrap:                   cfg.K9s.Logger.TextWrap,\n\t\tshowTime:                   cfg.K9s.Logger.ShowTime,\n\t\tshouldDisplayAllContainers: allContainers,\n\t\tcolumnLock:                 cfg.K9s.Logger.ColumnLock,\n\t}\n\n\tif cfg.K9s.Logger.DisableAutoscroll {\n\t\tl.scrollStatus = 0\n\t}\n\n\tl.StylesChanged(styles)\n\tstyles.AddListener(&l)\n\tl.SetTextAlign(tview.AlignCenter)\n\tl.SetDynamicColors(true)\n\n\treturn &l\n}\n\n// StylesChanged notifies listener the skin changed.\nfunc (l *LogIndicator) StylesChanged(styles *config.Styles) {\n\tl.SetBackgroundColor(styles.K9s.Views.Log.Indicator.BgColor.Color())\n\tl.SetTextColor(styles.K9s.Views.Log.Indicator.FgColor.Color())\n\tl.Refresh()\n}\n\n// AutoScroll reports the current scrolling status.\nfunc (l *LogIndicator) AutoScroll() bool {\n\treturn atomic.LoadInt32(&l.scrollStatus) == 1\n}\n\n// ColumnLock reports the current column lock mode.\nfunc (l *LogIndicator) ColumnLock() bool {\n\treturn l.columnLock\n}\n\n// Timestamp reports the current timestamp mode.\nfunc (l *LogIndicator) Timestamp() bool {\n\treturn l.showTime\n}\n\n// TextWrap reports the current wrap mode.\nfunc (l *LogIndicator) TextWrap() bool {\n\treturn l.textWrap\n}\n\n// FullScreen reports the current screen mode.\nfunc (l *LogIndicator) FullScreen() bool {\n\treturn l.fullScreen\n}\n\n// ToggleColumnLock toggles the current column lock mode.\nfunc (l *LogIndicator) ToggleColumnLock() {\n\tl.columnLock = !l.columnLock\n}\n\n// ToggleTimestamp toggles the current timestamp mode.\nfunc (l *LogIndicator) ToggleTimestamp() {\n\tl.showTime = !l.showTime\n}\n\n// ToggleFullScreen toggles the screen mode.\nfunc (l *LogIndicator) ToggleFullScreen() {\n\tl.fullScreen = !l.fullScreen\n\tl.Refresh()\n}\n\n// ToggleTextWrap toggles the wrap mode.\nfunc (l *LogIndicator) ToggleTextWrap() {\n\tl.textWrap = !l.textWrap\n\tl.Refresh()\n}\n\n// ToggleAutoScroll toggles the scroll mode.\nfunc (l *LogIndicator) ToggleAutoScroll() {\n\tvar val int32 = 1\n\tif l.AutoScroll() {\n\t\tval = 0\n\t}\n\tatomic.StoreInt32(&l.scrollStatus, val)\n\tl.Refresh()\n}\n\n// ToggleAllContainers toggles the all-containers mode.\nfunc (l *LogIndicator) ToggleAllContainers() {\n\tl.allContainers = !l.allContainers\n\tl.Refresh()\n}\n\nfunc (l *LogIndicator) reset() {\n\tl.Clear()\n\tl.indicator = l.indicator[:0]\n}\n\n// Refresh updates the view.\nfunc (l *LogIndicator) Refresh() {\n\tl.reset()\n\n\tvar (\n\t\ttoggleFmt    = \"[::b]%s:[\"\n\t\ttoggleOnFmt  = toggleFmt + string(l.styles.K9s.Views.Log.Indicator.ToggleOnColor) + \"::b]On[-::] %s\"\n\t\ttoggleOffFmt = toggleFmt + string(l.styles.K9s.Views.Log.Indicator.ToggleOffColor) + \"::d]Off[-::]%s\"\n\t)\n\n\tif l.shouldDisplayAllContainers {\n\t\tif l.allContainers {\n\t\t\tl.indicator = append(l.indicator, fmt.Sprintf(toggleOnFmt, \"AllContainers\", spacer)...)\n\t\t} else {\n\t\t\tl.indicator = append(l.indicator, fmt.Sprintf(toggleOffFmt, \"AllContainers\", spacer)...)\n\t\t}\n\t}\n\n\tif l.AutoScroll() {\n\t\tl.indicator = append(l.indicator, fmt.Sprintf(toggleOnFmt, \"Autoscroll\", spacer)...)\n\t} else {\n\t\tl.indicator = append(l.indicator, fmt.Sprintf(toggleOffFmt, \"Autoscroll\", spacer)...)\n\t}\n\n\tif l.ColumnLock() {\n\t\tl.indicator = append(l.indicator, fmt.Sprintf(toggleOnFmt, \"ColumnLock\", spacer)...)\n\t} else {\n\t\tl.indicator = append(l.indicator, fmt.Sprintf(toggleOffFmt, \"ColumnLock\", spacer)...)\n\t}\n\n\tif l.FullScreen() {\n\t\tl.indicator = append(l.indicator, fmt.Sprintf(toggleOnFmt, \"FullScreen\", spacer)...)\n\t} else {\n\t\tl.indicator = append(l.indicator, fmt.Sprintf(toggleOffFmt, \"FullScreen\", spacer)...)\n\t}\n\n\tif l.Timestamp() {\n\t\tl.indicator = append(l.indicator, fmt.Sprintf(toggleOnFmt, \"Timestamps\", spacer)...)\n\t} else {\n\t\tl.indicator = append(l.indicator, fmt.Sprintf(toggleOffFmt, \"Timestamps\", spacer)...)\n\t}\n\n\tif l.TextWrap() {\n\t\tl.indicator = append(l.indicator, fmt.Sprintf(toggleOnFmt, \"Wrap\", \"\")...)\n\t} else {\n\t\tl.indicator = append(l.indicator, fmt.Sprintf(toggleOffFmt, \"Wrap\", \"\")...)\n\t}\n\n\t_, _ = l.Write(l.indicator)\n}\n"
  },
  {
    "path": "internal/view/log_indicator_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestLogIndicatorRefresh(t *testing.T) {\n\tdefaults := config.NewStyles()\n\tuu := map[string]struct {\n\t\tli *view.LogIndicator\n\t\te  string\n\t}{\n\t\t\"all-containers\": {\n\t\t\tview.NewLogIndicator(config.NewConfig(nil), defaults, true), \"[::b]AllContainers:[gray::d]Off[-::]     [::b]Autoscroll:[limegreen::b]On[-::]      [::b]ColumnLock:[gray::d]Off[-::]     [::b]FullScreen:[gray::d]Off[-::]     [::b]Timestamps:[gray::d]Off[-::]     [::b]Wrap:[gray::d]Off[-::]\\n\",\n\t\t},\n\t\t\"plain\": {\n\t\t\tview.NewLogIndicator(config.NewConfig(nil), defaults, false), \"[::b]Autoscroll:[limegreen::b]On[-::]      [::b]ColumnLock:[gray::d]Off[-::]     [::b]FullScreen:[gray::d]Off[-::]     [::b]Timestamps:[gray::d]Off[-::]     [::b]Wrap:[gray::d]Off[-::]\\n\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tu.li.Refresh()\n\t\t\tassert.Equal(t, u.e, u.li.GetText(false))\n\t\t})\n\t}\n}\n\nfunc BenchmarkLogIndicatorRefresh(b *testing.B) {\n\tdefaults := config.NewStyles()\n\tv := view.NewLogIndicator(config.NewConfig(nil), defaults, true)\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor range b.N {\n\t\tv.Refresh()\n\t}\n}\n"
  },
  {
    "path": "internal/view/log_int_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestLogAutoScroll(t *testing.T) {\n\topts := dao.LogOptions{\n\t\tPath:            \"fred/p1\",\n\t\tContainer:       \"blee\",\n\t\tSingleContainer: true,\n\t}\n\tv := NewLog(client.PodGVR, &opts)\n\trequire.NoError(t, v.Init(makeContext(t)))\n\tii := dao.NewLogItems()\n\tii.Add(dao.NewLogItemFromString(\"blee\"), dao.NewLogItemFromString(\"bozo\"))\n\tv.GetModel().Set(ii)\n\tv.GetModel().Notify()\n\n\tassert.Len(t, v.Hints(), 18)\n\n\tv.toggleAutoScrollCmd(nil)\n\tassert.Equal(t, \"Autoscroll:Off     ColumnLock:Off     FullScreen:Off     Timestamps:Off     Wrap:Off\", v.Indicator().GetText(true))\n}\n\nfunc TestLogColumnLock(t *testing.T) {\n\topts := dao.LogOptions{\n\t\tPath:      \"fred/p1\",\n\t\tContainer: \"blee\",\n\t}\n\tv := NewLog(client.PodGVR, &opts)\n\trequire.NoError(t, v.Init(makeContext(t)))\n\n\tbuff := dao.NewLogItems()\n\tfor i := range 100 {\n\t\tbuff.Add(dao.NewLogItemFromString(fmt.Sprintf(\"line-%d\\n\", i)))\n\t}\n\tv.GetModel().Set(buff)\n\n\tv.toggleColumnLockCmd(nil)\n\tconst column = 2\n\tv.Logs().ScrollTo(-1, column)\n\tv.toggleAutoScrollCmd(nil)\n\n\tr, c := v.Logs().GetScrollOffset()\n\tassert.Equal(t, -1, r)\n\tassert.Equal(t, column, c)\n}\n\nfunc TestLogViewNav(t *testing.T) {\n\topts := dao.LogOptions{\n\t\tPath:      \"fred/p1\",\n\t\tContainer: \"blee\",\n\t}\n\tv := NewLog(client.PodGVR, &opts)\n\trequire.NoError(t, v.Init(makeContext(t)))\n\n\tbuff := dao.NewLogItems()\n\tfor i := range 100 {\n\t\tbuff.Add(dao.NewLogItemFromString(fmt.Sprintf(\"line-%d\\n\", i)))\n\t}\n\tv.GetModel().Set(buff)\n\tv.toggleAutoScrollCmd(nil)\n\n\tr, _ := v.Logs().GetScrollOffset()\n\tassert.Equal(t, -1, r)\n}\n\nfunc TestLogViewClear(t *testing.T) {\n\topts := dao.LogOptions{\n\t\tPath:      \"fred/p1\",\n\t\tContainer: \"blee\",\n\t}\n\tv := NewLog(client.PodGVR, &opts)\n\trequire.NoError(t, v.Init(makeContext(t)))\n\n\tv.toggleAutoScrollCmd(nil)\n\tv.Logs().SetText(\"blee\\nblah\")\n\tv.Logs().Clear()\n\n\tassert.Empty(t, v.Logs().GetText(true))\n}\n\nfunc TestLogTimestamp(t *testing.T) {\n\topts := dao.LogOptions{\n\t\tPath:      \"fred/blee\",\n\t\tContainer: \"c1\",\n\t}\n\tl := NewLog(client.NewGVR(\"test\"), &opts)\n\trequire.NoError(t, l.Init(makeContext(t)))\n\tii := dao.NewLogItems()\n\tii.Add(\n\t\t&dao.LogItem{\n\t\t\tPod:       \"fred/blee\",\n\t\t\tContainer: \"c1\",\n\t\t\tBytes:     []byte(\"ttt Testing 1, 2, 3\\n\"),\n\t\t},\n\t)\n\tvar list logList\n\tl.GetModel().AddListener(&list)\n\tl.GetModel().Set(ii)\n\tl.SendKeys(ui.KeyT)\n\tl.Logs().Clear()\n\tll := make([][]byte, ii.Len())\n\tii.Lines(0, true, ll)\n\tl.Flush(ll)\n\n\tassert.Equal(t, fmt.Sprintf(\"%-30s %s\", \"ttt\", \"fred/blee c1 Testing 1, 2, 3\\n\"), l.Logs().GetText(true))\n\tassert.Equal(t, 2, list.change)\n\tassert.Equal(t, 2, list.clear)\n\tassert.Equal(t, 0, list.fail)\n}\n\nfunc TestLogFilter(t *testing.T) {\n\topts := dao.LogOptions{\n\t\tPath:      \"fred/blee\",\n\t\tContainer: \"c1\",\n\t}\n\tl := NewLog(client.NewGVR(\"test\"), &opts)\n\trequire.NoError(t, l.Init(makeContext(t)))\n\tbuff := dao.NewLogItems()\n\tbuff.Add(\n\t\tdao.NewLogItemFromString(\"duh\"),\n\t\tdao.NewLogItemFromString(\"zorg\"),\n\t)\n\tvar list logList\n\tl.GetModel().AddListener(&list)\n\tl.GetModel().Set(buff)\n\tl.SendKeys(ui.KeySlash)\n\tl.SendStrokes(\"zorg\")\n\n\tassert.Equal(t, \"duhzorg\", list.lines)\n\tassert.Equal(t, 1, list.change)\n\tassert.Equal(t, 1, list.clear)\n\tassert.Equal(t, 0, list.fail)\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\ntype logList struct {\n\tchange, clear, fail int\n\tlines               string\n}\n\nfunc (l *logList) LogChanged(ll [][]byte) {\n\tl.change++\n\tl.lines = \"\"\n\tfor _, line := range ll {\n\t\tl.lines += string(line)\n\t}\n}\nfunc (*logList) LogCanceled()      {}\nfunc (*logList) LogStop()          {}\nfunc (*logList) LogResume()        {}\nfunc (l *logList) LogCleared()     { l.clear++ }\nfunc (l *logList) LogFailed(error) { l.fail++ }\n"
  },
  {
    "path": "internal/view/log_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/config/mock\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/derailed/tview\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestLog(t *testing.T) {\n\topts := dao.LogOptions{\n\t\tPath:      \"fred/p1\",\n\t\tContainer: \"blee\",\n\t}\n\tv := view.NewLog(client.PodGVR, &opts)\n\trequire.NoError(t, v.Init(makeContext(t)))\n\n\tii := dao.NewLogItems()\n\tii.Add(dao.NewLogItemFromString(\"blee\\n\"), dao.NewLogItemFromString(\"bozo\\n\"))\n\tll := make([][]byte, ii.Len())\n\tii.Lines(0, false, ll)\n\tv.Flush(ll)\n\n\tassert.Equal(t, \"Waiting for logs...\\nblee\\nbozo\\n\", v.Logs().GetText(true))\n}\n\nfunc TestLogFlush(t *testing.T) {\n\topts := dao.LogOptions{\n\t\tPath:      \"fred/p1\",\n\t\tContainer: \"blee\",\n\t}\n\tv := view.NewLog(client.PodGVR, &opts)\n\trequire.NoError(t, v.Init(makeContext(t)))\n\n\titems := dao.NewLogItems()\n\titems.Add(\n\t\tdao.NewLogItemFromString(\"\\033[0;30mblee\\n\"),\n\t\tdao.NewLogItemFromString(\"\\033[0;32mBozo\\n\"),\n\t)\n\tll := make([][]byte, items.Len())\n\titems.Lines(0, false, ll)\n\tv.Flush(ll)\n\n\tassert.Equal(t, \"[orange::d]Waiting for logs...\\n[black::]blee\\n[green::]Bozo\\n\\n\", v.Logs().GetText(false))\n}\n\nfunc BenchmarkLogFlush(b *testing.B) {\n\topts := dao.LogOptions{\n\t\tPath:      \"fred/p1\",\n\t\tContainer: \"blee\",\n\t}\n\tv := view.NewLog(client.PodGVR, &opts)\n\t_ = v.Init(makeContext(b))\n\n\titems := dao.NewLogItems()\n\titems.Add(\n\t\tdao.NewLogItemFromString(\"\\033[0;30mblee\\n\"),\n\t\tdao.NewLogItemFromString(\"\\033[0;101mBozo\\n\"),\n\t\tdao.NewLogItemFromString(\"\\033[0;101mBozo\\n\"),\n\t)\n\tll := make([][]byte, items.Len())\n\titems.Lines(0, false, ll)\n\n\tb.ReportAllocs()\n\tb.ResetTimer()\n\tfor range b.N {\n\t\tv.Flush(ll)\n\t}\n}\n\nfunc TestLogAnsi(t *testing.T) {\n\tbuff := bytes.NewBufferString(\"\")\n\tw := tview.ANSIWriter(buff, \"white\", \"black\")\n\t_, _ = fmt.Fprintf(w, \"[YELLOW] ok\")\n\tassert.Equal(t, \"[YELLOW] ok\", buff.String())\n\n\tv := tview.NewTextView()\n\tv.SetDynamicColors(true)\n\taw := tview.ANSIWriter(v, \"white\", \"black\")\n\ts := \"[2019-03-27T15:05:15,246][INFO ][o.e.c.r.a.AllocationService] [es-0] Cluster health status changed from [YELLOW] to [GREEN] (reason: [shards started [[.monitoring-es-6-2019.03.27][0]]\"\n\t_, _ = fmt.Fprintf(aw, \"%s\", s)\n\tassert.Equal(t, s+\"\\n\", v.GetText(false))\n}\n\nfunc TestLogViewSave(t *testing.T) {\n\topts := dao.LogOptions{\n\t\tPath:      \"fred/p1\",\n\t\tContainer: \"blee\",\n\t}\n\tv := view.NewLog(client.PodGVR, &opts)\n\trequire.NoError(t, v.Init(makeContext(t)))\n\n\tapp := makeApp(t)\n\tii := dao.NewLogItems()\n\tii.Add(dao.NewLogItemFromString(\"blee\"), dao.NewLogItemFromString(\"bozo\"))\n\tll := make([][]byte, ii.Len())\n\tii.Lines(0, false, ll)\n\tv.Flush(ll)\n\n\tdd := \"/tmp/test-dumps/na\"\n\trequire.NoError(t, ensureDumpDir(dd))\n\tapp.Config.K9s.ScreenDumpDir = \"/tmp/test-dumps\"\n\tdir := app.Config.K9s.ContextScreenDumpDir()\n\tc1, err := os.ReadDir(dir)\n\trequire.NoError(t, err, \"Dir: %q\", dir)\n\tv.SaveCmd(nil)\n\tc2, err := os.ReadDir(dir)\n\trequire.NoError(t, err, \"Dir: %q\", dir)\n\tassert.Len(t, c2, len(c1)+1)\n}\n\nfunc TestAllContainerKeyBinding(t *testing.T) {\n\tuu := map[string]struct {\n\t\topts *dao.LogOptions\n\t\te    bool\n\t}{\n\t\t\"action-present\": {\n\t\t\topts: &dao.LogOptions{Path: \"\", DefaultContainer: \"container\"},\n\t\t\te:    true,\n\t\t},\n\t\t\"action-missing\": {\n\t\t\topts: &dao.LogOptions{},\n\t\t},\n\t}\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tv := view.NewLog(client.PodGVR, u.opts)\n\t\t\trequire.NoError(t, v.Init(makeContext(t)))\n\t\t\t_, got := v.Logs().Actions().Get(ui.KeyA)\n\t\t\tassert.Equal(t, u.e, got)\n\t\t})\n\t}\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc makeApp(t *testing.T) *view.App {\n\treturn view.NewApp(mock.NewMockConfig(t))\n}\n\nfunc ensureDumpDir(n string) error {\n\tconfig.AppDumpsDir = n\n\tif _, err := os.Stat(n); errors.Is(err, fs.ErrNotExist) {\n\t\treturn os.MkdirAll(n, 0700)\n\t}\n\tif err := os.RemoveAll(n); err != nil {\n\t\treturn err\n\t}\n\treturn os.MkdirAll(n, 0700)\n}\n"
  },
  {
    "path": "internal/view/logger.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n)\n\n// Logger represents a generic log viewer.\ntype Logger struct {\n\t*tview.TextView\n\n\tactions        *ui.KeyActions\n\tapp            *App\n\ttitle, subject string\n\tcmdBuff        *model.FishBuff\n}\n\n// NewLogger returns a logger viewer.\nfunc NewLogger(app *App) *Logger {\n\treturn &Logger{\n\t\tTextView: tview.NewTextView(),\n\t\tapp:      app,\n\t\tactions:  ui.NewKeyActions(),\n\t\tcmdBuff:  model.NewFishBuff('/', model.FilterBuffer),\n\t}\n}\n\n// Init initializes the viewer.\nfunc (l *Logger) Init(_ context.Context) error {\n\tif l.title != \"\" {\n\t\tl.SetBorder(true)\n\t}\n\tl.SetScrollable(true).SetWrap(true)\n\tl.SetDynamicColors(true)\n\tl.SetHighlightColor(tcell.ColorOrange)\n\tl.SetTitleColor(tcell.ColorAqua)\n\tl.SetInputCapture(l.keyboard)\n\tl.SetBorderPadding(0, 0, 1, 1)\n\n\tl.app.Styles.AddListener(l)\n\tl.StylesChanged(l.app.Styles)\n\n\tl.app.Prompt().SetModel(l.cmdBuff)\n\tl.cmdBuff.AddListener(l)\n\n\tl.bindKeys()\n\tl.SetInputCapture(l.keyboard)\n\n\treturn nil\n}\n\n// BufferChanged indicates the buffer was changed.\nfunc (*Logger) BufferChanged(_, _ string) {}\n\n// BufferCompleted indicates input was accepted.\nfunc (*Logger) BufferCompleted(_, _ string) {}\n\n// BufferActive indicates the buff activity changed.\nfunc (l *Logger) BufferActive(state bool, k model.BufferKind) {\n\tl.app.BufferActive(state, k)\n}\n\nfunc (l *Logger) bindKeys() {\n\tl.actions.Bulk(ui.KeyMap{\n\t\ttcell.KeyEscape: ui.NewKeyAction(\"Back\", l.resetCmd, false),\n\t\tui.KeyQ:         ui.NewKeyAction(\"Back\", l.resetCmd, false),\n\t\ttcell.KeyCtrlS:  ui.NewKeyAction(\"Save\", l.saveCmd, false),\n\t\tui.KeyC:         ui.NewKeyAction(\"Copy\", cpCmd(l.app.Flash(), l.TextView), true),\n\t\tui.KeySlash:     ui.NewSharedKeyAction(\"Filter Mode\", l.activateCmd, false),\n\t\ttcell.KeyDelete: ui.NewSharedKeyAction(\"Erase\", l.eraseCmd, false),\n\t})\n}\n\nfunc (l *Logger) keyboard(evt *tcell.EventKey) *tcell.EventKey {\n\tif a, ok := l.actions.Get(ui.AsKey(evt)); ok {\n\t\treturn a.Action(evt)\n\t}\n\n\treturn evt\n}\n\n// StylesChanged notifies the skin changed.\nfunc (l *Logger) StylesChanged(*config.Styles) {\n\tl.SetBackgroundColor(l.app.Styles.BgColor())\n\tl.SetTextColor(l.app.Styles.FgColor())\n\tl.SetBorderFocusColor(l.app.Styles.Frame().Border.FocusColor.Color())\n}\n\n// SetSubject updates the subject.\nfunc (l *Logger) SetSubject(s string) {\n\tl.subject = s\n}\n\n// Actions returns menu actions.\nfunc (l *Logger) Actions() *ui.KeyActions {\n\treturn l.actions\n}\n\n// Name returns the component name.\nfunc (l *Logger) Name() string { return l.title }\n\n// Start starts the view updater.\nfunc (*Logger) Start() {}\n\n// Stop terminates the updater.\nfunc (l *Logger) Stop() {\n\tl.app.Styles.RemoveListener(l)\n}\n\n// Hints returns menu hints.\nfunc (l *Logger) Hints() model.MenuHints {\n\treturn l.actions.Hints()\n}\n\n// ExtraHints returns additional hints.\nfunc (*Logger) ExtraHints() map[string]string {\n\treturn nil\n}\n\nfunc (l *Logger) activateCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif l.app.InCmdMode() {\n\t\treturn evt\n\t}\n\tl.app.ResetPrompt(l.cmdBuff)\n\n\treturn nil\n}\n\nfunc (l *Logger) eraseCmd(*tcell.EventKey) *tcell.EventKey {\n\tif !l.cmdBuff.IsActive() {\n\t\treturn nil\n\t}\n\tl.cmdBuff.Delete()\n\n\treturn nil\n}\n\nfunc (l *Logger) resetCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif !l.cmdBuff.InCmdMode() {\n\t\tl.cmdBuff.Reset()\n\t\treturn l.app.PrevCmd(evt)\n\t}\n\tl.cmdBuff.SetActive(false)\n\tl.cmdBuff.Reset()\n\n\treturn nil\n}\n\nfunc (l *Logger) saveCmd(*tcell.EventKey) *tcell.EventKey {\n\tif path, err := saveYAML(l.app.Config.K9s.ContextScreenDumpDir(), l.title, l.GetText(true)); err != nil {\n\t\tl.app.Flash().Err(err)\n\t} else {\n\t\tl.app.Flash().Infof(\"Log %s saved successfully!\", path)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/view/logs_extender.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// LogsExtender adds log actions to a given viewer.\ntype LogsExtender struct {\n\tResourceViewer\n\n\toptionsFn LogOptionsFunc\n}\n\n// NewLogsExtender returns a new extender.\nfunc NewLogsExtender(v ResourceViewer, f LogOptionsFunc) ResourceViewer {\n\tl := LogsExtender{\n\t\tResourceViewer: v,\n\t\toptionsFn:      f,\n\t}\n\tl.AddBindKeysFn(l.bindKeys)\n\n\treturn &l\n}\n\n// BindKeys injects new menu actions.\nfunc (l *LogsExtender) bindKeys(aa *ui.KeyActions) {\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyL: ui.NewKeyAction(\"Logs\", l.logsCmd(false), true),\n\t\tui.KeyP: ui.NewKeyAction(\"Logs Previous\", l.logsCmd(true), true),\n\t})\n}\n\nfunc (l *LogsExtender) logsCmd(prev bool) func(evt *tcell.EventKey) *tcell.EventKey {\n\treturn func(*tcell.EventKey) *tcell.EventKey {\n\t\tpath := l.GetTable().GetSelectedItem()\n\t\tif path == \"\" {\n\t\t\treturn nil\n\t\t}\n\t\tif !isResourcePath(path) {\n\t\t\tpath = l.GetTable().Path\n\t\t}\n\t\tl.showLogs(path, prev)\n\n\t\treturn nil\n\t}\n}\n\nfunc isResourcePath(p string) bool {\n\tns, n := client.Namespaced(p)\n\treturn ns != \"\" && n != \"\"\n}\n\nfunc (l *LogsExtender) showLogs(path string, prev bool) {\n\tns, _ := client.Namespaced(path)\n\t_, err := l.App().factory.CanForResource(ns, client.PodGVR, client.ListAccess)\n\tif err != nil {\n\t\tl.App().Flash().Err(err)\n\t\treturn\n\t}\n\topts := l.buildLogOpts(path, \"\", prev)\n\tif l.optionsFn != nil {\n\t\tif opts, err = l.optionsFn(prev); err != nil {\n\t\t\tl.App().Flash().Err(err)\n\t\t\treturn\n\t\t}\n\t}\n\tif err := l.App().inject(NewLog(l.GVR(), opts), false); err != nil {\n\t\tl.App().Flash().Err(err)\n\t}\n}\n\n// buildLogOpts(path, co, prev, false, config.DefaultLoggerTailCount),.\nfunc (l *LogsExtender) buildLogOpts(path, co string, prevLogs bool) *dao.LogOptions {\n\tcfg := l.App().Config.K9s.Logger\n\topts := dao.LogOptions{\n\t\tPath:          path,\n\t\tContainer:     co,\n\t\tLines:         cfg.TailCount,\n\t\tPrevious:      prevLogs,\n\t\tShowTimestamp: cfg.ShowTime,\n\t}\n\tif opts.Container == \"\" {\n\t\topts.AllContainers = true\n\t}\n\n\treturn &opts\n}\n\nfunc podLogOptions(app *App, fqn string, prev bool, m *metav1.ObjectMeta, spec *v1.PodSpec) *dao.LogOptions {\n\tvar (\n\t\tcc   = fetchContainers(m, spec, true)\n\t\tcfg  = app.Config.K9s.Logger\n\t\topts = dao.LogOptions{\n\t\t\tPath:            fqn,\n\t\t\tLines:           cfg.TailCount,\n\t\t\tSinceSeconds:    cfg.SinceSeconds,\n\t\t\tSingleContainer: len(cc) == 1,\n\t\t\tShowTimestamp:   cfg.ShowTime,\n\t\t\tPrevious:        prev,\n\t\t}\n\t)\n\tif c, ok := dao.GetDefaultContainer(m, spec); ok {\n\t\topts.Container, opts.DefaultContainer = c, c\n\t} else if len(cc) == 1 {\n\t\topts.Container = cc[0]\n\t} else {\n\t\topts.AllContainers = true\n\t}\n\n\treturn &opts\n}\n"
  },
  {
    "path": "internal/view/node.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/ui/dialog\"\n\t\"github.com/derailed/tcell/v2\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// Node represents a node view.\ntype Node struct {\n\tResourceViewer\n}\n\n// NewNode returns a new node view.\nfunc NewNode(gvr *client.GVR) ResourceViewer {\n\tn := Node{\n\t\tResourceViewer: NewBrowser(gvr),\n\t}\n\tn.AddBindKeysFn(n.bindKeys)\n\tn.GetTable().SetEnterFn(n.showPods)\n\tn.SetContextFn(n.nodeContext)\n\n\treturn &n\n}\n\nfunc (n *Node) nodeContext(ctx context.Context) context.Context {\n\treturn context.WithValue(ctx, internal.KeyPodCounting, !n.App().Config.K9s.DisablePodCounting)\n}\n\nfunc (n *Node) bindDangerousKeys(aa *ui.KeyActions) {\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyC: ui.NewKeyActionWithOpts(\n\t\t\t\"Cordon\",\n\t\t\tn.toggleCordonCmd(true),\n\t\t\tui.ActionOpts{\n\t\t\t\tVisible:   true,\n\t\t\t\tDangerous: true,\n\t\t\t},\n\t\t),\n\t\tui.KeyU: ui.NewKeyActionWithOpts(\n\t\t\t\"Uncordon\",\n\t\t\tn.toggleCordonCmd(false),\n\t\t\tui.ActionOpts{\n\t\t\t\tVisible:   true,\n\t\t\t\tDangerous: true,\n\t\t\t},\n\t\t),\n\t\tui.KeyR: ui.NewKeyActionWithOpts(\n\t\t\t\"Drain\",\n\t\t\tn.drainCmd,\n\t\t\tui.ActionOpts{\n\t\t\t\tVisible:   true,\n\t\t\t\tDangerous: true,\n\t\t\t},\n\t\t),\n\t})\n\tct, err := n.App().Config.K9s.ActiveContext()\n\tif err != nil {\n\t\tslog.Error(\"No active context located\", slogs.Error, err)\n\t\treturn\n\t}\n\tif ct.FeatureGates.NodeShell && n.App().Config.K9s.ShellPod != nil {\n\t\taa.Add(ui.KeyS, ui.NewKeyAction(\"Shell\", n.sshCmd, true))\n\t}\n}\n\nfunc (n *Node) bindKeys(aa *ui.KeyActions) {\n\tif !n.App().Config.IsReadOnly() {\n\t\tn.bindDangerousKeys(aa)\n\t}\n\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyY: ui.NewKeyAction(yamlAction, n.yamlCmd, true),\n\t})\n}\n\nfunc (n *Node) showPods(a *App, _ ui.Tabular, _ *client.GVR, path string) {\n\tshowPods(a, n.GetTable().GetSelectedItem(), nil, \"spec.nodeName=\"+path)\n}\n\nfunc (n *Node) drainCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tsels := n.GetTable().GetSelectedItems()\n\tif len(sels) == 0 {\n\t\treturn evt\n\t}\n\n\topts := dao.DrainOptions{\n\t\tGracePeriodSeconds: -1,\n\t\tTimeout:            5 * time.Second,\n\t}\n\tShowDrain(n, sels, opts, drainNode)\n\n\treturn nil\n}\n\nfunc drainNode(v ResourceViewer, sels []string, opts dao.DrainOptions) {\n\tres, err := dao.AccessorFor(v.App().factory, v.GVR())\n\tif err != nil {\n\t\tv.App().Flash().Err(err)\n\t\treturn\n\t}\n\tm, ok := res.(dao.NodeMaintainer)\n\tif !ok {\n\t\tv.App().Flash().Err(fmt.Errorf(\"expecting a maintainer for %q\", v.GVR()))\n\t\treturn\n\t}\n\n\tv.Stop()\n\tdefer v.Start()\n\t{\n\t\td := NewDetails(v.App(), \"Drain Progress\", \"nodes\", contentYAML, true)\n\t\tif err := v.App().inject(d, false); err != nil {\n\t\t\tv.App().Flash().Err(err)\n\t\t}\n\t\tfor _, sel := range sels {\n\t\t\tif err := m.Drain(sel, opts, d.GetWriter()); err != nil {\n\t\t\t\tv.App().Flash().Err(err)\n\t\t\t}\n\t\t}\n\t\tv.Refresh()\n\t}\n}\n\nfunc (n *Node) toggleCordonCmd(cordon bool) func(evt *tcell.EventKey) *tcell.EventKey {\n\treturn func(evt *tcell.EventKey) *tcell.EventKey {\n\t\tsels := n.GetTable().GetSelectedItems()\n\t\tif len(sels) == 0 {\n\t\t\treturn evt\n\t\t}\n\n\t\ttitle, msg := \"Confirm \", \"\"\n\t\tif cordon {\n\t\t\ttitle, msg = title+\"Cordon\", \"Cordon \"\n\t\t} else {\n\t\t\ttitle, msg = title+\"Uncordon\", \"Uncordon \"\n\t\t}\n\t\tif len(sels) == 1 {\n\t\t\tmsg += sels[0] + \"?\"\n\t\t} else {\n\t\t\tmsg += fmt.Sprintf(\"(%d) marked %s?\", len(sels), n.GVR().R())\n\t\t}\n\t\td := n.App().Styles.Dialog()\n\t\tdialog.ShowConfirm(&d, n.App().Content.Pages, title, msg, func() {\n\t\t\tres, err := dao.AccessorFor(n.App().factory, n.GVR())\n\t\t\tif err != nil {\n\t\t\t\tn.App().Flash().Err(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tm, ok := res.(dao.NodeMaintainer)\n\t\t\tif !ok {\n\t\t\t\tn.App().Flash().Err(fmt.Errorf(\"expecting a maintainer for %q\", n.GVR()))\n\t\t\t\treturn\n\t\t\t}\n\t\t\tfor _, s := range sels {\n\t\t\t\tif err := m.ToggleCordon(s, cordon); err != nil {\n\t\t\t\t\tn.App().Flash().Err(err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tn.Refresh()\n\t\t}, func() {})\n\n\t\treturn nil\n\t}\n}\n\nfunc (n *Node) sshCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := n.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\n\tn.Stop()\n\tdefer n.Start()\n\t_, node := client.Namespaced(path)\n\tlaunchNodeShell(n, n.App(), node)\n\n\treturn nil\n}\n\nfunc (n *Node) yamlCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := n.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\n\tn.Stop()\n\tdefer n.Start()\n\tctx, cancel := context.WithTimeout(context.Background(), n.App().Conn().Config().CallTimeout())\n\tdefer cancel()\n\n\tsel := n.GetTable().GetSelectedItem()\n\tgvr := n.GVR().GVR()\n\tdial, err := n.App().factory.Client().DynDial()\n\tif err != nil {\n\t\tn.App().Flash().Err(err)\n\t\treturn nil\n\t}\n\to, err := dial.Resource(gvr).Get(ctx, sel, metav1.GetOptions{})\n\tif err != nil {\n\t\tn.App().Flash().Errf(\"Unable to get resource %q -- %s\", n.GVR(), err)\n\t\treturn nil\n\t}\n\n\traw, err := dao.ToYAML(o, false)\n\tif err != nil {\n\t\tn.App().Flash().Errf(\"Unable to marshal resource %s\", err)\n\t\treturn nil\n\t}\n\n\tdetails := NewDetails(n.App(), yamlAction, sel, contentYAML, true).Update(raw)\n\tif err := n.App().inject(details, false); err != nil {\n\t\tn.App().Flash().Err(err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/view/ns.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"k8s.io/apimachinery/pkg/util/sets\"\n)\n\nconst (\n\tfavNSIndicator     = \"+\"\n\tdefaultNSIndicator = \"(*)\"\n)\n\n// Namespace represents a namespace viewer.\ntype Namespace struct {\n\tResourceViewer\n}\n\n// NewNamespace returns a new viewer.\nfunc NewNamespace(gvr *client.GVR) ResourceViewer {\n\tn := Namespace{\n\t\tResourceViewer: NewBrowser(gvr),\n\t}\n\tn.GetTable().SetDecorateFn(n.decorate)\n\tn.GetTable().SetEnterFn(n.switchNs)\n\tn.AddBindKeysFn(n.bindKeys)\n\n\treturn &n\n}\n\nfunc (n *Namespace) bindKeys(aa *ui.KeyActions) {\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyU: ui.NewKeyAction(\"Use\", n.useNsCmd, true),\n\t})\n}\n\nfunc (n *Namespace) switchNs(app *App, _ ui.Tabular, _ *client.GVR, path string) {\n\tn.useNamespace(path)\n\t_, ns := client.Namespaced(path)\n\tapp.gotoResource(client.PodGVR.String()+\" \"+ns, \"\", false, true)\n}\n\nfunc (n *Namespace) useNsCmd(*tcell.EventKey) *tcell.EventKey {\n\tpath := n.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn nil\n\t}\n\tn.useNamespace(path)\n\n\treturn nil\n}\n\nfunc (n *Namespace) useNamespace(fqn string) {\n\t_, ns := client.Namespaced(fqn)\n\tif client.CleanseNamespace(n.App().Config.ActiveNamespace()) == ns {\n\t\treturn\n\t}\n\tif err := n.App().switchNS(ns); err != nil {\n\t\tn.App().Flash().Err(err)\n\t\treturn\n\t}\n\tif err := n.App().Config.SetActiveNamespace(ns); err != nil {\n\t\tn.App().Flash().Err(err)\n\t\treturn\n\t}\n}\n\nfunc (n *Namespace) decorate(td *model1.TableData) {\n\tif n.App().Conn() == nil || td.RowCount() == 0 {\n\t\treturn\n\t}\n\t// checks if all ns is in the list if not add it.\n\tif _, ok := td.FindRow(client.NamespaceAll); !ok {\n\t\ttd.AddRow(model1.RowEvent{\n\t\t\tKind: model1.EventUnchanged,\n\t\t\tRow: model1.Row{\n\t\t\t\tID:     client.NamespaceAll,\n\t\t\t\tFields: model1.Fields{client.NamespaceAll, \"Active\", \"\", \"\", \"\"},\n\t\t\t},\n\t\t},\n\t\t)\n\t}\n\n\tvar (\n\t\tfavs     = sets.New(n.App().Config.FavNamespaces()...)\n\t\tactiveNS = n.App().Config.ActiveNamespace()\n\t)\n\ttd.RowsRange(func(i int, re model1.RowEvent) bool {\n\t\t_, n := client.Namespaced(re.Row.ID)\n\t\tif favs.Has(n) {\n\t\t\tre.Row.Fields[0] += favNSIndicator\n\t\t}\n\t\tif n == activeNS {\n\t\t\tre.Row.Fields[0] += defaultNSIndicator\n\t\t}\n\t\tre.Kind = model1.EventUnchanged\n\t\ttd.SetRow(i, re)\n\t\treturn true\n\t})\n}\n"
  },
  {
    "path": "internal/view/ns_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNSCleanser(t *testing.T) {\n\tns := view.NewNamespace(client.NsGVR)\n\n\trequire.NoError(t, ns.Init(makeCtx(t)))\n\tassert.Equal(t, \"Namespaces\", ns.Name())\n\tassert.Len(t, ns.Hints(), 8)\n}\n"
  },
  {
    "path": "internal/view/owner_extender.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/ui/dialog\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/go-errors/errors\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n)\n\n// OwnerExtender adds owner actions to a given viewer.\ntype OwnerExtender struct {\n\tResourceViewer\n}\n\n// NewOwnerExtender returns a new extender.\nfunc NewOwnerExtender(r ResourceViewer) ResourceViewer {\n\tv := &OwnerExtender{ResourceViewer: r}\n\tv.AddBindKeysFn(v.bindKeys)\n\n\treturn v\n}\n\nfunc (v *OwnerExtender) bindKeys(aa *ui.KeyActions) {\n\taa.Add(ui.KeyShiftJ, ui.NewKeyAction(\"Jump Owner\", v.ownerCmd, true))\n}\n\nfunc (v *OwnerExtender) ownerCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := v.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\n\tif err := v.findOwnerFor(path); err != nil {\n\t\tslog.Warn(\"Unable to jump to owner of resource\",\n\t\t\tslogs.FQN, path,\n\t\t\tslogs.Error, err,\n\t\t)\n\t\tv.App().Flash().Warnf(\"Unable to jump owner: %s\", err)\n\t}\n\treturn nil\n}\n\nfunc (v *OwnerExtender) findOwnerFor(path string) error {\n\tres, err := dao.AccessorFor(v.App().factory, v.GVR())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\to, err := res.Get(v.defaultCtx(), path)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tu, ok := v.asUnstructuredObject(o)\n\tif !ok {\n\t\treturn errors.Errorf(\"unsupported object type: %t\", o)\n\t}\n\n\tns, _ := client.Namespaced(path)\n\townerReferences := u.GetOwnerReferences()\n\tif len(ownerReferences) == 1 {\n\t\treturn v.jumpOwner(ns, &ownerReferences[0])\n\t} else if len(ownerReferences) > 1 {\n\t\towners := make([]string, 0, len(ownerReferences))\n\t\tfor idx, ownerRef := range ownerReferences {\n\t\t\towners = append(owners, fmt.Sprintf(\"%d: %s\", idx, ownerRef.Kind))\n\t\t}\n\n\t\td := v.App().Styles.Dialog()\n\t\tdialog.ShowSelection(&d, v.App().Content.Pages, \"Jump To\", owners, func(index int) {\n\t\t\tif index >= 0 {\n\t\t\t\terr = v.jumpOwner(ns, &ownerReferences[index])\n\t\t\t}\n\t\t})\n\t\treturn err\n\t}\n\n\treturn errors.Errorf(\"no owner found\")\n}\n\nfunc (v *OwnerExtender) jumpOwner(ns string, owner *metav1.OwnerReference) error {\n\tgv, err := schema.ParseGroupVersion(owner.APIVersion)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tgvr, namespaced, found := dao.MetaAccess.GVK2GVR(gv, owner.Kind)\n\tif !found {\n\t\treturn errors.Errorf(\"unsupported GVK: %s/%s\", owner.APIVersion, owner.Kind)\n\t}\n\n\tvar ownerFQN string\n\tif namespaced {\n\t\townerFQN = client.FQN(ns, owner.Name)\n\t} else {\n\t\townerFQN = owner.Name\n\t}\n\n\tv.App().gotoResource(gvr.String(), ownerFQN, false, true)\n\treturn nil\n}\n\nfunc (v *OwnerExtender) defaultCtx() context.Context {\n\treturn context.WithValue(context.Background(), internal.KeyFactory, v.App().factory)\n}\n\nfunc (*OwnerExtender) asUnstructuredObject(o runtime.Object) (*unstructured.Unstructured, bool) {\n\tswitch v := o.(type) {\n\tcase *unstructured.Unstructured:\n\t\treturn v, true\n\tcase *render.PodWithMetrics:\n\t\treturn v.Raw, true\n\tdefault:\n\t\treturn nil, false\n\t}\n}\n"
  },
  {
    "path": "internal/view/page_stack.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/ui\"\n)\n\n// PageStack represents a stack of pages.\ntype PageStack struct {\n\t*ui.Pages\n\n\tapp *App\n}\n\n// NewPageStack returns a new page stack.\nfunc NewPageStack() *PageStack {\n\treturn &PageStack{\n\t\tPages: ui.NewPages(),\n\t}\n}\n\n// Init initializes the view.\nfunc (p *PageStack) Init(ctx context.Context) (err error) {\n\tif p.app, err = extractApp(ctx); err != nil {\n\t\treturn err\n\t}\n\tp.AddListener(p)\n\n\treturn nil\n}\n\n// StackPushed notifies a new page was added.\nfunc (p *PageStack) StackPushed(c model.Component) {\n\tc.Start()\n\tp.app.SetFocus(c)\n}\n\n// StackPopped notifies a page was removed.\nfunc (p *PageStack) StackPopped(o, top model.Component) {\n\to.Stop()\n\tp.StackTop(top)\n}\n\n// StackTop notifies for the top component.\nfunc (p *PageStack) StackTop(top model.Component) {\n\tif top == nil {\n\t\treturn\n\t}\n\ttop.Start()\n\tp.app.SetFocus(top)\n}\n"
  },
  {
    "path": "internal/view/pf.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"regexp\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/perf\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/ui/dialog\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\n// PortForward presents active portforward viewer.\ntype PortForward struct {\n\tResourceViewer\n\n\tbench *perf.Benchmark\n}\n\n// NewPortForward returns a new viewer.\nfunc NewPortForward(gvr *client.GVR) ResourceViewer {\n\tp := PortForward{\n\t\tResourceViewer: NewBrowser(gvr),\n\t}\n\tp.GetTable().SetBorderFocusColor(tcell.ColorDodgerBlue)\n\tp.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorDodgerBlue).Attributes(tcell.AttrNone))\n\tp.GetTable().SetSortCol(ageCol, true)\n\tp.SetContextFn(p.portForwardContext)\n\tp.AddBindKeysFn(p.bindKeys)\n\n\treturn &p\n}\n\nfunc (p *PortForward) portForwardContext(ctx context.Context) context.Context {\n\tif bc := p.App().BenchFile; bc != \"\" {\n\t\treturn context.WithValue(ctx, internal.KeyBenchCfg, p.App().BenchFile)\n\t}\n\n\treturn ctx\n}\n\nfunc (p *PortForward) bindKeys(aa *ui.KeyActions) {\n\taa.Delete(ui.KeyShiftS)\n\taa.Bulk(ui.KeyMap{\n\t\ttcell.KeyEnter: ui.NewKeyAction(\"View Benchmarks\", p.showBenchCmd, true),\n\t\tui.KeyB:        ui.NewKeyAction(\"Benchmark Run/Stop\", p.toggleBenchCmd, true),\n\t\ttcell.KeyCtrlD: ui.NewKeyAction(\"Delete\", p.deleteCmd, true),\n\t\tui.KeyShiftP:   ui.NewKeyAction(\"Sort Ports\", p.GetTable().SortColCmd(\"PORTS\", true), false),\n\t\tui.KeyShiftU:   ui.NewKeyAction(\"Sort URL\", p.GetTable().SortColCmd(\"URL\", true), false),\n\t})\n}\n\nfunc (p *PortForward) showBenchCmd(*tcell.EventKey) *tcell.EventKey {\n\tb := NewBenchmark(client.BeGVR)\n\tb.SetContextFn(p.getContext)\n\tif err := p.App().inject(b, false); err != nil {\n\t\tp.App().Flash().Err(err)\n\t}\n\n\treturn nil\n}\n\nfunc (p *PortForward) getContext(ctx context.Context) context.Context {\n\tctx = context.WithValue(ctx, internal.KeyDir, benchDir(p.App().Config))\n\tpath := p.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn ctx\n\t}\n\n\treturn context.WithValue(ctx, internal.KeyPath, path)\n}\n\nfunc (p *PortForward) toggleBenchCmd(*tcell.EventKey) *tcell.EventKey {\n\tif p.bench != nil {\n\t\tp.App().Status(model.FlashErr, \"Benchmark Canceled!\")\n\t\tp.bench.Cancel()\n\t\tp.App().ClearStatus(true)\n\t\treturn nil\n\t}\n\n\tpath := p.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn nil\n\t}\n\tcfg := dao.BenchConfigFor(p.App().BenchFile, path)\n\tcfg.Name = path\n\n\tr, _ := p.GetTable().GetSelection()\n\tslog.Debug(\"Port forward namespace\", slogs.Namespace, p.GetTable().GetModel().GetNamespace())\n\tcol := 3\n\tif client.IsAllNamespaces(p.GetTable().GetModel().GetNamespace()) {\n\t\tcol = 4\n\t}\n\tbase := ui.TrimCell(p.GetTable().SelectTable, r, col)\n\tvar err error\n\tp.bench, err = perf.NewBenchmark(base, p.App().version, &cfg)\n\tif err != nil {\n\t\tp.App().Flash().Errf(\"Bench failed %v\", err)\n\t\tp.App().ClearStatus(false)\n\t\treturn nil\n\t}\n\n\tp.App().Status(model.FlashWarn, \"Benchmark in progress...\")\n\tgo func() {\n\t\tif err := p.runBenchmark(); err != nil {\n\t\t\tslog.Error(\"Benchmark run failed\", slogs.Error, err)\n\t\t}\n\t}()\n\n\treturn nil\n}\n\nfunc (p *PortForward) runBenchmark() error {\n\tslog.Debug(\"Bench starting...\")\n\n\tct, err := p.App().Config.K9s.ActiveContext()\n\tif err != nil {\n\t\treturn err\n\t}\n\tname := p.App().Config.K9s.ActiveContextName()\n\tp.bench.Run(ct.ClusterName, name, func() {\n\t\tslog.Debug(\"Benchmark Completed!\", slogs.Name, name)\n\t\tp.App().QueueUpdate(func() {\n\t\t\tif p.bench.Canceled() {\n\t\t\t\tp.App().Status(model.FlashInfo, \"Benchmark canceled\")\n\t\t\t} else {\n\t\t\t\tp.App().Status(model.FlashInfo, \"Benchmark Completed!\")\n\t\t\t\tp.bench.Cancel()\n\t\t\t}\n\t\t\tp.bench = nil\n\t\t\tgo func() {\n\t\t\t\t<-time.After(2 * time.Second)\n\t\t\t\tp.App().QueueUpdate(func() { p.App().ClearStatus(true) })\n\t\t\t}()\n\t\t})\n\t})\n\n\treturn nil\n}\n\nfunc (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif !p.GetTable().CmdBuff().Empty() {\n\t\tp.GetTable().CmdBuff().Reset()\n\t\treturn nil\n\t}\n\n\tselections := p.GetTable().GetSelectedItems()\n\tif len(selections) == 0 {\n\t\treturn evt\n\t}\n\n\tp.Stop()\n\tdefer p.Start()\n\tvar msg string\n\tif len(selections) > 1 {\n\t\tmsg = fmt.Sprintf(\"Delete %d marked %s?\", len(selections), p.GVR())\n\t} else if h, err := pfToHuman(selections[0]); err == nil {\n\t\tmsg = fmt.Sprintf(\"Delete %s %s?\", p.GVR().R(), h)\n\t} else {\n\t\tp.App().Flash().Err(err)\n\t\treturn nil\n\t}\n\n\td := p.App().Styles.Dialog()\n\tdialog.ShowConfirm(&d, p.App().Content.Pages, \"Delete\", msg, func() {\n\t\tfor _, s := range selections {\n\t\t\tvar pf dao.PortForward\n\t\t\tpf.Init(p.App().factory, client.PfGVR)\n\t\t\tif err := pf.Delete(context.Background(), s, nil, dao.DefaultGrace); err != nil {\n\t\t\t\tp.App().Flash().Err(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tp.App().Flash().Infof(\"Successfully deleted %d PortForward!\", len(selections))\n\t\tp.GetTable().Refresh()\n\t}, func() {})\n\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nvar selRx = regexp.MustCompile(`\\A([\\w-]+)/([\\w-]+)\\|([\\w-]+)?\\|(\\d+):(\\d+)`)\n\nfunc pfToHuman(s string) (string, error) {\n\tmm := selRx.FindStringSubmatch(s)\n\tif len(mm) < 6 {\n\t\treturn \"\", fmt.Errorf(\"unable to parse selection %s\", s)\n\t}\n\n\treturn fmt.Sprintf(\"%s::%s %s->%s\", mm[2], mm[3], mm[4], mm[5]), nil\n}\n"
  },
  {
    "path": "internal/view/pf_dialog.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"math\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/port\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tview\"\n)\n\nconst portForwardKey = \"portforward\"\n\n// PortForwardCB represents a port-forward callback function.\ntype PortForwardCB func(ResourceViewer, string, port.PortTunnels) error\n\n// ShowPortForwards pops a port forwarding configuration dialog.\nfunc ShowPortForwards(v ResourceViewer, path string, ports port.ContainerPortSpecs, aa port.Annotations, okFn PortForwardCB) {\n\tstyles := v.App().Styles.Dialog()\n\n\tf := tview.NewForm()\n\tf.SetItemPadding(0)\n\tf.SetButtonsAlign(tview.AlignCenter).\n\t\tSetButtonBackgroundColor(styles.ButtonBgColor.Color()).\n\t\tSetButtonTextColor(styles.ButtonFgColor.Color()).\n\t\tSetLabelColor(styles.LabelFgColor.Color()).\n\t\tSetFieldTextColor(styles.FieldFgColor.Color()).\n\t\tSetFieldBackgroundColor(styles.BgColor.Color())\n\n\tpf, err := aa.PreferredPorts(ports)\n\tif err != nil {\n\t\tslog.Warn(\"Unable to resolve preferred ports\",\n\t\t\tslogs.FQN, path,\n\t\t\tslogs.Error, err,\n\t\t)\n\t}\n\n\tp1, p2 := pf.ToPortSpec(ports)\n\tfieldLen := int(math.Max(30, float64(len(p1))))\n\tf.AddInputField(\"Container Port:\", p1, fieldLen, nil, nil)\n\tf.AddInputField(\"Local Port:\", p2, fieldLen, nil, nil)\n\tcoField := f.GetFormItemByLabel(\"Container Port:\").(*tview.InputField)\n\tloField := f.GetFormItemByLabel(\"Local Port:\").(*tview.InputField)\n\tif coField.GetText() == \"\" {\n\t\tcoField.SetPlaceholder(\"Enter a container name::port\")\n\t}\n\tcoField.SetChangedFunc(func(s string) {\n\t\tp := extractPort(s)\n\t\tloField.SetText(p)\n\t\tp2 = p\n\t})\n\tif loField.GetText() == \"\" {\n\t\tloField.SetPlaceholder(\"Enter a local port\")\n\t}\n\taddress := v.App().Config.K9s.PortForwardAddress\n\tf.AddInputField(\"Address:\", address, fieldLen, nil, func(h string) {\n\t\taddress = h\n\t})\n\tfor i := range 3 {\n\t\tif field, ok := f.GetFormItem(i).(*tview.InputField); ok {\n\t\t\tfield.SetLabelColor(styles.LabelFgColor.Color())\n\t\t\tfield.SetFieldTextColor(styles.FieldFgColor.Color())\n\t\t}\n\t}\n\n\tf.AddButton(\"OK\", func() {\n\t\tif coField.GetText() == \"\" || loField.GetText() == \"\" {\n\t\t\tv.App().Flash().Err(fmt.Errorf(\"container to local port mismatch\"))\n\t\t\treturn\n\t\t}\n\t\ttt, err := port.ToTunnels(address, coField.GetText(), loField.GetText())\n\t\tif err != nil {\n\t\t\tv.App().Flash().Err(err)\n\t\t\treturn\n\t\t}\n\t\tif err := okFn(v, path, tt); err != nil {\n\t\t\tv.App().Flash().Err(err)\n\t\t}\n\t})\n\tpages := v.App().Content.Pages\n\tf.AddButton(\"Cancel\", func() {\n\t\tDismissPortForwards(v, pages)\n\t})\n\tfor i := range 2 {\n\t\tif b := f.GetButton(i); b != nil {\n\t\t\tb.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color())\n\t\t\tb.SetLabelColorActivated(styles.ButtonFocusFgColor.Color())\n\t\t}\n\t}\n\n\tmodal := tview.NewModalForm(\"<PortForward>\", f)\n\tmsg := path\n\tif len(ports) > 1 {\n\t\tmsg += \"\\n\\nExposed Ports:\\n\" + ports.Dump()\n\t}\n\tmodal.SetText(msg)\n\tmodal.SetTextColor(styles.FgColor.Color())\n\tmodal.SetBackgroundColor(styles.BgColor.Color())\n\tmodal.SetDoneFunc(func(int, string) {\n\t\tDismissPortForwards(v, pages)\n\t})\n\n\tpages.AddPage(portForwardKey, modal, false, true)\n\tpages.ShowPage(portForwardKey)\n\tv.App().SetFocus(pages.GetPrimitive(portForwardKey))\n}\n\n// DismissPortForwards dismiss the port forward dialog.\nfunc DismissPortForwards(v ResourceViewer, p *ui.Pages) {\n\tp.RemovePage(portForwardKey)\n\tv.App().SetFocus(p.CurrentPage().Item)\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc extractPort(p string) string {\n\ttokens := strings.Split(p, \"::\")\n\tif len(tokens) < 2 {\n\t\tports := strings.Split(p, \",\")\n\t\tfor _, t := range ports {\n\t\t\tif _, err := strconv.Atoi(strings.TrimSpace(t)); err != nil {\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t}\n\t\treturn p\n\t}\n\n\treturn tokens[1]\n}\n"
  },
  {
    "path": "internal/view/pf_dialog_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestExtractPort(t *testing.T) {\n\tuu := map[string]struct {\n\t\tportSpec, e string\n\t}{\n\t\t\"full\": {\n\t\t\tportSpec: \"co::8000\",\n\t\t\te:        \"8000\",\n\t\t},\n\t\t\"toast\": {\n\t\t\tportSpec: \"co:8000\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, extractPort(u.portSpec))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/view/pf_extender.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/port\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/watch\"\n\t\"github.com/derailed/tcell/v2\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/client-go/tools/portforward\"\n)\n\n// PortForwardExtender adds port-forward extensions.\ntype PortForwardExtender struct {\n\tResourceViewer\n}\n\n// NewPortForwardExtender returns a new extender.\nfunc NewPortForwardExtender(r ResourceViewer) ResourceViewer {\n\tp := PortForwardExtender{ResourceViewer: r}\n\tp.AddBindKeysFn(p.bindKeys)\n\n\treturn &p\n}\n\nfunc (p *PortForwardExtender) bindKeys(aa *ui.KeyActions) {\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyF:      ui.NewKeyAction(\"Show PortForward\", p.showPFCmd, true),\n\t\tui.KeyShiftF: ui.NewKeyAction(\"Port-Forward\", p.portFwdCmd, true),\n\t})\n}\n\nfunc (p *PortForwardExtender) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := p.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\n\tpodName, err := p.fetchPodName(path)\n\tif err != nil {\n\t\tp.App().Flash().Err(err)\n\t\treturn nil\n\t}\n\tif err := ensurePodPortFwdAllowed(p.App().factory, podName); err != nil {\n\t\tp.App().Flash().Err(err)\n\t\treturn nil\n\t}\n\tif err := showFwdDialog(p, podName, startFwdCB); err != nil {\n\t\tp.App().Flash().Err(err)\n\t}\n\n\treturn nil\n}\n\nfunc (p *PortForwardExtender) showPFCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := p.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\n\tpodName, err := p.fetchPodName(path)\n\tif err != nil {\n\t\tp.App().Flash().Err(err)\n\t\treturn nil\n\t}\n\n\tif !p.App().factory.Forwarders().IsPodForwarded(podName) {\n\t\tp.App().Flash().Errf(\"no port-forward defined\")\n\t\treturn nil\n\t}\n\n\tpf := NewPortForward(client.PfGVR)\n\tpf.SetContextFn(p.portForwardContext)\n\tif err := p.App().inject(pf, false); err != nil {\n\t\tp.App().Flash().Err(err)\n\t}\n\n\treturn nil\n}\n\nfunc (p *PortForwardExtender) fetchPodName(path string) (string, error) {\n\tres, err := dao.AccessorFor(p.App().factory, p.GVR())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tctrl, ok := res.(dao.Controller)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"expecting a controller resource for %q\", p.GVR())\n\t}\n\n\treturn ctrl.Pod(path)\n}\n\nfunc (p *PortForwardExtender) portForwardContext(ctx context.Context) context.Context {\n\tif bc := p.App().BenchFile; bc != \"\" {\n\t\tctx = context.WithValue(ctx, internal.KeyBenchCfg, p.App().BenchFile)\n\t}\n\n\treturn context.WithValue(ctx, internal.KeyPath, p.GetTable().GetSelectedItem())\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc ensurePodPortFwdAllowed(factory dao.Factory, podName string) error {\n\tpod, err := fetchPod(factory, podName)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif pod.Status.Phase != v1.PodRunning {\n\t\treturn fmt.Errorf(\"pod must be running. Current status=%v\", pod.Status.Phase)\n\t}\n\n\treturn nil\n}\n\nfunc runForward(v ResourceViewer, pf watch.Forwarder, f *portforward.PortForwarder) {\n\tv.App().factory.AddForwarder(pf)\n\n\tv.App().QueueUpdateDraw(func() {\n\t\tDismissPortForwards(v, v.App().Content.Pages)\n\t})\n\n\tpf.SetActive(true)\n\tif err := f.ForwardPorts(); err != nil {\n\t\tv.App().Flash().Warnf(\"PortForward failed for %s: %s. Deleting!\", pf.ID(), err)\n\t}\n\tv.App().QueueUpdateDraw(func() {\n\t\tv.App().factory.DeleteForwarder(pf.ID())\n\t\tpf.SetActive(false)\n\t})\n}\n\nfunc startFwdCB(v ResourceViewer, path string, pts port.PortTunnels) error {\n\tif err := pts.CheckAvailable(context.Background()); err != nil {\n\t\treturn err\n\t}\n\n\ttt := make([]string, 0, len(pts))\n\tfor _, pt := range pts {\n\t\tif _, ok := v.App().factory.ForwarderFor(dao.PortForwardID(path, pt.Container, pt.PortMap())); ok {\n\t\t\treturn fmt.Errorf(\"port-forward is already active on pod %s\", path)\n\t\t}\n\t\tpf := dao.NewPortForwarder(v.App().factory)\n\t\tfwd, err := pf.Start(path, pt)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tslog.Debug(\">>> Starting port forward\",\n\t\t\tslogs.PFID, pf.ID(),\n\t\t\tslogs.PFTunnel, pt,\n\t\t)\n\t\tgo runForward(v, pf, fwd)\n\t\ttt = append(tt, pt.LocalPort)\n\t}\n\tif len(tt) == 1 {\n\t\tv.App().Flash().Infof(\"PortForward activated %s\", tt[0])\n\t\treturn nil\n\t}\n\tv.App().Flash().Infof(\"PortForwards activated %s\", strings.Join(tt, \",\"))\n\n\treturn nil\n}\n\nfunc showFwdDialog(v ResourceViewer, path string, cb PortForwardCB) error {\n\tmm, anns, err := fetchPodPorts(v.App().factory, path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tports := make(port.ContainerPortSpecs, 0, len(mm))\n\tfor co, pp := range mm {\n\t\tfor _, p := range pp {\n\t\t\tif p.Protocol != v1.ProtocolTCP {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tports = append(ports, port.NewPortSpec(co, p.Name, p.ContainerPort))\n\t\t}\n\t}\n\tif spec, ok := anns[port.K9sAutoPortForwardsKey]; ok {\n\t\tpfs, err := port.ParsePFs(spec)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tpts, err := pfs.ToTunnels(v.App().Config.K9s.PortForwardAddress, ports, port.IsPortFree)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn startFwdCB(v, path, pts)\n\t}\n\tShowPortForwards(v, path, ports, anns, cb)\n\n\treturn nil\n}\n\nfunc fetchPodPorts(f *watch.Factory, path string) (ports map[string][]v1.ContainerPort, anns map[string]string, err error) {\n\tslog.Debug(\"Fetching ports on pod\", slogs.FQN, path)\n\to, err := f.Get(client.PodGVR, path, true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tvar pod v1.Pod\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tpp := make(map[string][]v1.ContainerPort, len(pod.Spec.Containers))\n\tfor i := range pod.Spec.Containers {\n\t\tpp[pod.Spec.Containers[i].Name] = pod.Spec.Containers[i].Ports\n\t}\n\n\treturn pp, pod.Annotations, nil\n}\n"
  },
  {
    "path": "internal/view/pf_extender_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"errors\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/watch\"\n\t\"github.com/stretchr/testify/require\"\n\tcorev1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/client-go/informers\"\n)\n\nfunc TestEnsurePodPortFwdAllowed(t *testing.T) {\n\tuu := map[string]struct {\n\t\tpodExists   bool\n\t\tpodPhase    corev1.PodPhase\n\t\texpectError bool\n\t}{\n\t\t\"pod-not-exist\": {\n\t\t\texpectError: true,\n\t\t},\n\t\t\"pod-pending\": {\n\t\t\tpodExists:   true,\n\t\t\tpodPhase:    corev1.PodPending,\n\t\t\texpectError: true,\n\t\t},\n\t\t\"pod-running\": {\n\t\t\tpodExists:   true,\n\t\t\tpodPhase:    corev1.PodRunning,\n\t\t\texpectError: false,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tf := testFactory{}\n\t\t\tif u.podExists {\n\t\t\t\tf.expectedGet = &unstructured.Unstructured{\n\t\t\t\t\tObject: map[string]any{\n\t\t\t\t\t\t\"status\": map[string]any{\n\t\t\t\t\t\t\t\"phase\": u.podPhase,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t}\n\n\t\t\terr := ensurePodPortFwdAllowed(f, \"ns/name\")\n\t\t\tif u.expectError {\n\t\t\t\trequire.Error(t, err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\trequire.NoError(t, err)\n\t\t})\n\t}\n}\n\ntype testFactory struct {\n\texpectedGet runtime.Object\n}\n\nvar _ dao.Factory = testFactory{}\n\nfunc (testFactory) Client() client.Connection {\n\treturn nil\n}\nfunc (t testFactory) Get(*client.GVR, string, bool, labels.Selector) (runtime.Object, error) {\n\tif t.expectedGet != nil {\n\t\treturn t.expectedGet, nil\n\t}\n\n\treturn nil, errors.New(\"not found\")\n}\nfunc (testFactory) List(*client.GVR, string, bool, labels.Selector) ([]runtime.Object, error) {\n\treturn nil, nil\n}\nfunc (testFactory) ForResource(string, *client.GVR) (informers.GenericInformer, error) {\n\treturn nil, nil\n}\nfunc (testFactory) CanForResource(string, *client.GVR, []string) (informers.GenericInformer, error) {\n\treturn nil, nil\n}\nfunc (testFactory) Forwarders() watch.Forwarders {\n\treturn nil\n}\nfunc (testFactory) WaitForCacheSync()      {}\nfunc (testFactory) DeleteForwarder(string) {}\n"
  },
  {
    "path": "internal/view/pf_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPortForwardNew(t *testing.T) {\n\tpf := view.NewPortForward(client.PfGVR)\n\n\trequire.NoError(t, pf.Init(makeCtx(t)))\n\tassert.Equal(t, \"PortForwards\", pf.Name())\n\tassert.Len(t, pf.Hints(), 11)\n}\n"
  },
  {
    "path": "internal/view/picker.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n)\n\n// Picker represents a container picker.\ntype Picker struct {\n\t*tview.List\n\n\tactions ui.KeyActions\n}\n\n// NewPicker returns a new picker.\nfunc NewPicker() *Picker {\n\treturn &Picker{\n\t\tList:    tview.NewList(),\n\t\tactions: *ui.NewKeyActions(),\n\t}\n}\n\nfunc (*Picker) SetCommand(*cmd.Interpreter)            {}\nfunc (*Picker) SetFilter(string, bool)                 {}\nfunc (*Picker) SetLabelSelector(labels.Selector, bool) {}\n\n// Init initializes the view.\nfunc (p *Picker) Init(ctx context.Context) error {\n\tapp, err := extractApp(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpickerView := app.Styles.Views().Picker\n\tp.actions.Add(tcell.KeyEscape, ui.NewKeyAction(\"Back\", app.PrevCmd, true))\n\n\tp.SetBorder(true)\n\tp.SetMainTextColor(pickerView.MainColor.Color())\n\tp.ShowSecondaryText(false)\n\tp.SetShortcutColor(pickerView.ShortcutColor.Color())\n\tp.SetSelectedBackgroundColor(pickerView.FocusColor.Color())\n\tp.SetTitle(fmt.Sprintf(\" [%s::b]Containers Picker \", app.Styles.Frame().Title.FgColor.String()))\n\n\tp.SetInputCapture(func(evt *tcell.EventKey) *tcell.EventKey {\n\t\tif a, ok := p.actions.Get(evt.Key()); ok {\n\t\t\ta.Action(evt)\n\t\t\tevt = nil\n\t\t}\n\t\treturn evt\n\t})\n\n\treturn nil\n}\n\n// InCmdMode checks if prompt is active.\nfunc (*Picker) InCmdMode() bool {\n\treturn false\n}\n\n// Start starts the view.\nfunc (*Picker) Start() {}\n\n// Stop stops the view.\nfunc (*Picker) Stop() {}\n\n// Name returns the component name.\nfunc (*Picker) Name() string { return \"picker\" }\n\n// Hints returns the view hints.\nfunc (p *Picker) Hints() model.MenuHints {\n\treturn p.actions.Hints()\n}\n\n// ExtraHints returns additional hints.\nfunc (*Picker) ExtraHints() map[string]string {\n\treturn nil\n}\n\nfunc (p *Picker) populate(ss []string) {\n\tp.Clear()\n\tfor i, s := range ss {\n\t\tp.AddItem(s, \"Select a container\", rune('a'+i), nil)\n\t}\n}\n"
  },
  {
    "path": "internal/view/pod.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/fs\"\n\t\"log/slog\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/ui/dialog\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/fatih/color\"\n\tv1 \"k8s.io/api/core/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/cli-runtime/pkg/genericclioptions\"\n)\n\nconst (\n\twindowsOS        = \"windows\"\n\tpowerShell       = \"powershell\"\n\tosSelector       = \"kubernetes.io/os\"\n\tosBetaSelector   = \"beta.\" + osSelector\n\ttrUpload         = \"Upload\"\n\ttrDownload       = \"Download\"\n\tpfIndicator      = \"[orange::b]Ⓕ\"\n\tdefaultTxRetries = 999\n\tmagicPrompt      = \"Yes Please!\"\n)\n\n// Pod represents a pod viewer.\ntype Pod struct {\n\tResourceViewer\n}\n\n// NewPod returns a new viewer.\nfunc NewPod(gvr *client.GVR) ResourceViewer {\n\tvar p Pod\n\tp.ResourceViewer = NewPortForwardExtender(\n\t\tNewOwnerExtender(\n\t\t\tNewVulnerabilityExtender(\n\t\t\t\tNewImageExtender(\n\t\t\t\t\tNewLogsExtender(NewBrowser(gvr), p.logOptions),\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t)\n\tp.AddBindKeysFn(p.bindKeys)\n\tp.GetTable().SetEnterFn(p.showContainers)\n\tp.GetTable().SetDecorateFn(p.portForwardIndicator)\n\n\treturn &p\n}\n\nfunc (p *Pod) portForwardIndicator(data *model1.TableData) {\n\tff := p.App().factory.Forwarders()\n\n\tdefer decorateCpuMemHeaderRows(p.App(), data)\n\tidx, ok := data.IndexOfHeader(\"PF\")\n\tif !ok {\n\t\treturn\n\t}\n\n\tdata.RowsRange(func(_ int, re model1.RowEvent) bool {\n\t\tif ff.IsPodForwarded(re.Row.ID) {\n\t\t\tre.Row.Fields[idx] = pfIndicator\n\t\t}\n\t\treturn true\n\t})\n}\n\nfunc (p *Pod) bindDangerousKeys(aa *ui.KeyActions) {\n\taa.Bulk(ui.KeyMap{\n\t\ttcell.KeyCtrlK: ui.NewKeyActionWithOpts(\n\t\t\t\"Kill\",\n\t\t\tp.killCmd,\n\t\t\tui.ActionOpts{\n\t\t\t\tVisible:   true,\n\t\t\t\tDangerous: true,\n\t\t\t}),\n\t\tui.KeyS: ui.NewKeyActionWithOpts(\n\t\t\t\"Shell\",\n\t\t\tp.shellCmd,\n\t\t\tui.ActionOpts{\n\t\t\t\tVisible:   true,\n\t\t\t\tDangerous: true,\n\t\t\t}),\n\t\tui.KeyA: ui.NewKeyActionWithOpts(\n\t\t\t\"Attach\",\n\t\t\tp.attachCmd,\n\t\t\tui.ActionOpts{\n\t\t\t\tVisible:   true,\n\t\t\t\tDangerous: true,\n\t\t\t}),\n\t\tui.KeyT: ui.NewKeyActionWithOpts(\n\t\t\t\"Transfer\",\n\t\t\tp.transferCmd,\n\t\t\tui.ActionOpts{\n\t\t\t\tVisible:   true,\n\t\t\t\tDangerous: true,\n\t\t\t}),\n\t\tui.KeyZ: ui.NewKeyActionWithOpts(\n\t\t\t\"Sanitize\",\n\t\t\tp.sanitizeCmd,\n\t\t\tui.ActionOpts{\n\t\t\t\tVisible:   true,\n\t\t\t\tDangerous: true,\n\t\t\t}),\n\t})\n}\n\nfunc (p *Pod) bindKeys(aa *ui.KeyActions) {\n\tif !p.App().Config.IsReadOnly() {\n\t\tp.bindDangerousKeys(aa)\n\t}\n\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyO: ui.NewKeyAction(\"Show Node\", p.showNode, true),\n\t})\n}\n\nfunc (p *Pod) logOptions(prev bool) (*dao.LogOptions, error) {\n\tpath := p.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn nil, errors.New(\"you must provide a selection\")\n\t}\n\n\tpod, err := fetchPod(p.App().factory, path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn podLogOptions(p.App(), path, prev, &pod.ObjectMeta, &pod.Spec), nil\n}\n\nfunc (p *Pod) showContainers(app *App, _ ui.Tabular, _ *client.GVR, _ string) {\n\tco := NewContainer(client.CoGVR)\n\tco.SetContextFn(p.coContext)\n\tif err := app.inject(co, false); err != nil {\n\t\tapp.Flash().Err(err)\n\t}\n}\n\nfunc (p *Pod) coContext(ctx context.Context) context.Context {\n\treturn context.WithValue(ctx, internal.KeyPath, p.GetTable().GetSelectedItem())\n}\n\n// Handlers...\n\nfunc (p *Pod) showNode(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := p.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\tpod, err := fetchPod(p.App().factory, path)\n\tif err != nil {\n\t\tp.App().Flash().Err(err)\n\t\treturn nil\n\t}\n\tif pod.Spec.NodeName == \"\" {\n\t\tp.App().Flash().Err(errors.New(\"no node assigned\"))\n\t\treturn nil\n\t}\n\tno := NewNode(client.NodeGVR)\n\tno.SetInstance(pod.Spec.NodeName)\n\tif err := p.App().inject(no, false); err != nil {\n\t\tp.App().Flash().Err(err)\n\t}\n\n\treturn nil\n}\n\nfunc (p *Pod) killCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tselections := p.GetTable().GetSelectedItems()\n\tif len(selections) == 0 {\n\t\treturn evt\n\t}\n\n\tres, err := dao.AccessorFor(p.App().factory, p.GVR())\n\tif err != nil {\n\t\tp.App().Flash().Err(err)\n\t\treturn nil\n\t}\n\tnuker, ok := res.(dao.Nuker)\n\tif !ok {\n\t\tp.App().Flash().Err(fmt.Errorf(\"expecting a nuker for %q\", p.GVR()))\n\t\treturn nil\n\t}\n\tif len(selections) > 1 {\n\t\tp.App().Flash().Infof(\"Delete %d marked %s\", len(selections), p.GVR())\n\t} else {\n\t\tp.App().Flash().Infof(\"Delete resource %s %s\", p.GVR(), selections[0])\n\t}\n\tp.GetTable().ShowDeleted()\n\tfor _, path := range selections {\n\t\tif err := nuker.Delete(context.Background(), path, nil, dao.NowGrace); err != nil {\n\t\t\tp.App().Flash().Errf(\"Delete failed with %s\", err)\n\t\t} else {\n\t\t\tp.App().factory.DeleteForwarder(path)\n\t\t}\n\t\tp.GetTable().DeleteMark(path)\n\t}\n\tp.Refresh()\n\n\treturn nil\n}\n\nfunc (p *Pod) shellCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := p.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\n\tif !podIsRunning(p.App().factory, path) {\n\t\tp.App().Flash().Errf(\"%s is not in a running state\", path)\n\t\treturn nil\n\t}\n\n\tif err := containerShellIn(p.App(), p, path, \"\"); err != nil {\n\t\tp.App().Flash().Err(err)\n\t}\n\n\treturn nil\n}\n\nfunc (p *Pod) attachCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := p.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\n\tif !podIsRunning(p.App().factory, path) {\n\t\tp.App().Flash().Errf(\"%s is not in a happy state\", path)\n\t\treturn nil\n\t}\n\n\tif err := containerAttachIn(p.App(), p, path, \"\"); err != nil {\n\t\tp.App().Flash().Err(err)\n\t}\n\n\treturn nil\n}\n\nfunc (p *Pod) sanitizeCmd(*tcell.EventKey) *tcell.EventKey {\n\tres, err := dao.AccessorFor(p.App().factory, p.GVR())\n\tif err != nil {\n\t\tp.App().Flash().Err(err)\n\t\treturn nil\n\t}\n\ts, ok := res.(dao.Sanitizer)\n\tif !ok {\n\t\tp.App().Flash().Err(fmt.Errorf(\"expecting a sanitizer for %q\", p.GVR()))\n\t\treturn nil\n\t}\n\n\tmsg := fmt.Sprintf(\"Sanitize deletes all pods in completed/error state\\nPlease enter [orange::b]%s[-::-] to proceed.\", magicPrompt)\n\tdialog.ShowConfirmAck(p.App().App, p.App().Content.Pages, magicPrompt, true, \"Sanitize\", msg, func() {\n\t\tctx, cancel := context.WithTimeout(context.Background(), 5*p.App().Conn().Config().CallTimeout())\n\t\tdefer cancel()\n\t\ttotal, err := s.Sanitize(ctx, p.GetTable().GetModel().GetNamespace())\n\t\tif err != nil {\n\t\t\tp.App().Flash().Err(err)\n\t\t\treturn\n\t\t}\n\t\tp.App().Flash().Infof(\"Sanitized %d %s\", total, p.GVR())\n\t\tp.Refresh()\n\t}, func() {})\n\n\treturn nil\n}\n\nfunc (p *Pod) transferCmd(*tcell.EventKey) *tcell.EventKey {\n\tpath := p.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn nil\n\t}\n\n\tns, n := client.Namespaced(path)\n\tack := func(args dialog.TransferArgs) bool {\n\t\tlocal := args.To\n\t\tif !args.Download {\n\t\t\tlocal = args.From\n\t\t}\n\t\tif _, err := os.Stat(local); !args.Download && errors.Is(err, fs.ErrNotExist) {\n\t\t\tp.App().Flash().Err(err)\n\t\t\treturn false\n\t\t}\n\n\t\topts := make([]string, 0, 10)\n\t\topts = append(opts,\n\t\t\t\"cp\",\n\t\t\tstrings.TrimSpace(args.From),\n\t\t\tstrings.TrimSpace(args.To),\n\t\t\tfmt.Sprintf(\"--no-preserve=%t\", args.NoPreserve),\n\t\t\tfmt.Sprintf(\"--retries=%d\", args.Retries),\n\t\t)\n\t\tif args.CO != \"\" {\n\t\t\topts = append(opts, \"-c=\"+args.CO)\n\t\t}\n\t\topts = append(opts, fmt.Sprintf(\"--retries=%d\", args.Retries))\n\n\t\tcliOpts := shellOpts{\n\t\t\tbackground: true,\n\t\t\targs:       opts,\n\t\t}\n\t\top := trUpload\n\t\tif args.Download {\n\t\t\top = trDownload\n\t\t}\n\n\t\tfqn := path + \":\" + args.CO\n\t\tif err := runK(p.App(), &cliOpts); err != nil {\n\t\t\tp.App().cowCmd(err.Error())\n\t\t} else {\n\t\t\tp.App().Flash().Infof(\"%s successful on %s!\", op, fqn)\n\t\t}\n\t\treturn true\n\t}\n\n\tpod, err := fetchPod(p.App().factory, path)\n\tif err != nil {\n\t\tp.App().Flash().Err(err)\n\t\treturn nil\n\t}\n\n\topts := dialog.TransferDialogOpts{\n\t\tTitle:      \"Transfer\",\n\t\tContainers: fetchContainers(&pod.ObjectMeta, &pod.Spec, false),\n\t\tMessage:    \"Download Files\",\n\t\tPod:        fmt.Sprintf(\"%s/%s:\", ns, n),\n\t\tAck:        ack,\n\t\tRetries:    defaultTxRetries,\n\t\tCancel:     func() {},\n\t}\n\td := p.App().Styles.Dialog()\n\tdialog.ShowUploads(&d, p.App().Content.Pages, &opts)\n\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc containerShellIn(a *App, comp model.Component, path, co string) error {\n\tif co != \"\" {\n\t\tresumeShellIn(a, comp, path, co)\n\t\treturn nil\n\t}\n\n\tpod, err := fetchPod(a.factory, path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif dco, ok := dao.GetDefaultContainer(&pod.ObjectMeta, &pod.Spec); ok {\n\t\tresumeShellIn(a, comp, path, dco)\n\t\treturn nil\n\t}\n\n\tcc := fetchContainers(&pod.ObjectMeta, &pod.Spec, false)\n\tif len(cc) == 1 {\n\t\tresumeShellIn(a, comp, path, cc[0])\n\t\treturn nil\n\t}\n\n\tpicker := NewPicker()\n\tpicker.populate(cc)\n\tpicker.SetSelectedFunc(func(_ int, co, _ string, _ rune) {\n\t\tresumeShellIn(a, comp, path, co)\n\t})\n\n\treturn a.inject(picker, false)\n}\n\nfunc resumeShellIn(a *App, c model.Component, path, co string) {\n\tvar err error\n\tc.Stop()\n\tdefer func() {\n\t\tc.Start()\n\t\ta.QueueUpdate(func() {\n\t\t\tif err != nil {\n\t\t\t\ta.Flash().Errf(\"Shell exec failed: %s\", err)\n\t\t\t}\n\t\t})\n\t}()\n\n\terr = shellIn(a, path, co)\n}\n\nfunc shellIn(a *App, fqn, co string) error {\n\tplatform, err := getPodOS(a.factory, fqn)\n\tif err != nil {\n\t\tslog.Warn(\"OS detection failed (assuming linux)\", slogs.Error, err)\n\t\tplatform = \"linux\"\n\t}\n\n\targs := computeShellArgs(fqn, co, a.Conn().Config().Flags(), platform)\n\tc := color.New(color.BgGreen).Add(color.FgBlack).Add(color.Bold)\n\treturn runK(a, &shellOpts{\n\t\tclear:  true,\n\t\tbanner: c.Sprintf(bannerFmt, fqn, co),\n\t\targs:   args},\n\t)\n}\n\nfunc containerAttachIn(a *App, comp model.Component, path, co string) error {\n\tif co != \"\" {\n\t\tresumeAttachIn(a, comp, path, co)\n\t\treturn nil\n\t}\n\n\tpod, err := fetchPod(a.factory, path)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcc := fetchContainers(&pod.ObjectMeta, &pod.Spec, false)\n\tif len(cc) == 1 {\n\t\tresumeAttachIn(a, comp, path, cc[0])\n\t\treturn nil\n\t}\n\tpicker := NewPicker()\n\tpicker.populate(cc)\n\tpicker.SetSelectedFunc(func(_ int, co, _ string, _ rune) {\n\t\tresumeAttachIn(a, comp, path, co)\n\t})\n\tif err := a.inject(picker, false); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc resumeAttachIn(a *App, c model.Component, path, co string) {\n\tc.Stop()\n\tdefer c.Start()\n\n\tattachIn(a, path, co)\n}\n\nfunc attachIn(a *App, path, co string) {\n\targs := buildShellArgs(\"attach\", path, co, a.Conn().Config().Flags())\n\tc := color.New(color.BgGreen).Add(color.FgBlack).Add(color.Bold)\n\tif err := runK(a, &shellOpts{clear: true, banner: c.Sprintf(bannerFmt, path, co), args: args}); err != nil {\n\t\ta.Flash().Errf(\"Attach exec failed: %s\", err)\n\t}\n}\n\nfunc computeShellArgs(path, co string, flags *genericclioptions.ConfigFlags, platform string) []string {\n\targs := buildShellArgs(\"exec\", path, co, flags)\n\tif platform == windowsOS {\n\t\treturn append(args, \"--\", powerShell)\n\t}\n\n\treturn append(args, \"--\", \"sh\", \"-c\", shellCheck)\n}\n\nfunc isFlagSet(flag *string) (string, bool) {\n\tif flag == nil || *flag == \"\" {\n\t\treturn \"\", false\n\t}\n\n\treturn *flag, true\n}\n\nfunc buildShellArgs(cmd, path, co string, flags *genericclioptions.ConfigFlags) []string {\n\targs := make([]string, 0, 15)\n\n\targs = append(args, cmd, \"-it\")\n\tns, po := client.Namespaced(path)\n\tif ns != client.BlankNamespace {\n\t\targs = append(args, \"-n\", ns)\n\t}\n\targs = append(args, po)\n\tif flags != nil {\n\t\tif v, ok := isFlagSet(flags.KubeConfig); ok {\n\t\t\targs = append(args, \"--kubeconfig\", v)\n\t\t}\n\t\tif v, ok := isFlagSet(flags.Context); ok {\n\t\t\targs = append(args, \"--context\", v)\n\t\t}\n\t\tif v, ok := isFlagSet(flags.BearerToken); ok {\n\t\t\targs = append(args, \"--token\", v)\n\t\t}\n\t}\n\tif co != \"\" {\n\t\targs = append(args, \"-c\", co)\n\t}\n\n\treturn args\n}\n\nfunc fetchContainers(meta *metav1.ObjectMeta, spec *v1.PodSpec, allContainers bool) []string {\n\tnn := make([]string, 0, len(spec.Containers)+len(spec.EphemeralContainers)+len(spec.InitContainers))\n\t// put the default container as the first entry\n\tdefaultContainer, ok := dao.GetDefaultContainer(meta, spec)\n\tif ok {\n\t\tnn = append(nn, defaultContainer)\n\t}\n\n\tfor i := range spec.Containers {\n\t\tif spec.Containers[i].Name != defaultContainer {\n\t\t\tnn = append(nn, spec.Containers[i].Name)\n\t\t}\n\t}\n\n\tfor i := range spec.InitContainers {\n\t\tisSidecar := spec.InitContainers[i].RestartPolicy != nil && *spec.InitContainers[i].RestartPolicy == v1.ContainerRestartPolicyAlways\n\t\tif allContainers || isSidecar {\n\t\t\tnn = append(nn, spec.InitContainers[i].Name)\n\t\t}\n\t}\n\tfor i := range spec.EphemeralContainers {\n\t\tnn = append(nn, spec.EphemeralContainers[i].Name)\n\t}\n\n\treturn nn\n}\n\nfunc fetchPod(f dao.Factory, path string) (*v1.Pod, error) {\n\to, err := f.Get(client.PodGVR, path, true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar pod v1.Pod\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &pod, nil\n}\n\nfunc podIsRunning(f dao.Factory, fqn string) bool {\n\tpo, err := fetchPod(f, fqn)\n\tif err != nil {\n\t\tslog.Error(\"Unable to fetch pod\",\n\t\t\tslogs.FQN, fqn,\n\t\t\tslogs.Error, err,\n\t\t)\n\t\treturn false\n\t}\n\n\tvar re render.Pod\n\treturn re.Phase(po.DeletionTimestamp, &po.Spec, &po.Status) == render.Running\n}\n\nfunc getPodOS(f dao.Factory, fqn string) (string, error) {\n\tpo, err := fetchPod(f, fqn)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif podOS, ok := osFromSelector(po.Spec.NodeSelector); ok {\n\t\treturn podOS, nil\n\t}\n\n\tnode, err := dao.FetchNode(context.Background(), f, po.Spec.NodeName)\n\tif err == nil {\n\t\tif nodeOS, ok := osFromSelector(node.Labels); ok {\n\t\t\treturn nodeOS, nil\n\t\t}\n\t}\n\n\treturn \"\", errors.New(\"no os information available\")\n}\n\nfunc osFromSelector(s map[string]string) (string, bool) {\n\tif platform, ok := s[osBetaSelector]; ok {\n\t\treturn platform, ok\n\t}\n\tplatform, ok := s[osSelector]\n\n\treturn platform, ok\n}\n"
  },
  {
    "path": "internal/view/pod_int_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"k8s.io/cli-runtime/pkg/genericclioptions\"\n)\n\nfunc newStr(s string) *string {\n\treturn &s\n}\n\nfunc TestComputeShellArgs(t *testing.T) {\n\tuu := map[string]struct {\n\t\tfqn, co, os string\n\t\tcfg         *genericclioptions.ConfigFlags\n\t\te           string\n\t}{\n\t\t\"config\": {\n\t\t\tfqn: \"fred/blee\",\n\t\t\tco:  \"c1\",\n\t\t\tos:  \"darwin\",\n\t\t\tcfg: &genericclioptions.ConfigFlags{\n\t\t\t\tKubeConfig: newStr(\"coolConfig\"),\n\t\t\t},\n\t\t\te: \"exec -it -n fred blee --kubeconfig coolConfig -c c1 -- sh -c \" + shellCheck,\n\t\t},\n\n\t\t\"no-config\": {\n\t\t\tfqn: \"fred/blee\",\n\t\t\tco:  \"c1\",\n\t\t\tos:  \"linux\",\n\t\t\te:   \"exec -it -n fred blee -c c1 -- sh -c \" + shellCheck,\n\t\t},\n\n\t\t\"empty-config\": {\n\t\t\tfqn: \"fred/blee\",\n\t\t\tcfg: new(genericclioptions.ConfigFlags),\n\t\t\te:   \"exec -it -n fred blee -- sh -c \" + shellCheck,\n\t\t},\n\n\t\t\"single-container\": {\n\t\t\tfqn: \"fred/blee\",\n\t\t\tos:  \"linux\",\n\t\t\tcfg: new(genericclioptions.ConfigFlags),\n\t\t\te:   \"exec -it -n fred blee -- sh -c \" + shellCheck,\n\t\t},\n\n\t\t\"windows\": {\n\t\t\tfqn: \"fred/blee\",\n\t\t\tco:  \"c1\",\n\t\t\tos:  windowsOS,\n\t\t\tcfg: new(genericclioptions.ConfigFlags),\n\t\t\te:   \"exec -it -n fred blee -c c1 -- powershell\",\n\t\t},\n\n\t\t\"full\": {\n\t\t\tfqn: \"fred/blee\",\n\t\t\tco:  \"c1\",\n\t\t\tos:  windowsOS,\n\t\t\tcfg: &genericclioptions.ConfigFlags{\n\t\t\t\tKubeConfig:  newStr(\"coolConfig\"),\n\t\t\t\tContext:     newStr(\"coolContext\"),\n\t\t\t\tBearerToken: newStr(\"coolToken\"),\n\t\t\t},\n\t\t\te: \"exec -it -n fred blee --kubeconfig coolConfig --context coolContext --token coolToken -c c1 -- powershell\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\targs := computeShellArgs(u.fqn, u.co, u.cfg, u.os)\n\t\t\tassert.Equal(t, u.e, strings.Join(args, \" \"))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/view/pod_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config/mock\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPodNew(t *testing.T) {\n\tpo := view.NewPod(client.PodGVR)\n\n\trequire.NoError(t, po.Init(makeCtx(t)))\n\tassert.Equal(t, \"Pods\", po.Name())\n\tassert.Len(t, po.Hints(), 19)\n}\n\n// Helpers...\n\nfunc makeCtx(t testing.TB) context.Context {\n\tcfg := mock.NewMockConfig(t)\n\treturn context.WithValue(context.Background(), internal.KeyApp, view.NewApp(cfg))\n}\n"
  },
  {
    "path": "internal/view/policy.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\nconst (\n\tgroup = \"Group\"\n\tuser  = \"User\"\n\tsa    = \"ServiceAccount\"\n)\n\n// Policy presents a RBAC rules viewer based on what a given user/group or sa can do.\ntype Policy struct {\n\tResourceViewer\n\n\tsubjectKind, subjectName string\n}\n\n// NewPolicy returns a new viewer.\nfunc NewPolicy(_ *App, subject, name string) *Policy {\n\tp := Policy{\n\t\tResourceViewer: NewBrowser(client.PolGVR),\n\t\tsubjectKind:    subject,\n\t\tsubjectName:    name,\n\t}\n\tp.AddBindKeysFn(p.bindKeys)\n\tp.GetTable().SetSortCol(\"API-GROUP\", false)\n\tp.SetContextFn(p.subjectCtx)\n\tp.GetTable().SetEnterFn(blankEnterFn)\n\n\treturn &p\n}\n\nfunc (p *Policy) subjectCtx(ctx context.Context) context.Context {\n\tctx = context.WithValue(ctx, internal.KeySubjectKind, mapSubject(p.subjectKind))\n\tctx = context.WithValue(ctx, internal.KeyPath, mapSubject(p.subjectKind)+\":\"+p.subjectName)\n\treturn context.WithValue(ctx, internal.KeySubjectName, p.subjectName)\n}\n\nfunc (*Policy) bindKeys(aa *ui.KeyActions) {\n\taa.Delete(ui.KeyShiftA, tcell.KeyCtrlSpace, ui.KeySpace)\n}\n\nfunc mapSubject(subject string) string {\n\tswitch subject {\n\tcase \"g\":\n\t\treturn group\n\tcase \"s\":\n\t\treturn sa\n\tcase \"u\":\n\t\treturn user\n\tdefault:\n\t\treturn subject\n\t}\n}\n"
  },
  {
    "path": "internal/view/priorityclass.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\n// PriorityClass presents a priority class viewer.\ntype PriorityClass struct {\n\tResourceViewer\n}\n\n// NewPriorityClass returns a new viewer.\nfunc NewPriorityClass(gvr *client.GVR) ResourceViewer {\n\ts := PriorityClass{\n\t\tResourceViewer: NewBrowser(gvr),\n\t}\n\ts.AddBindKeysFn(s.bindKeys)\n\n\treturn &s\n}\n\nfunc (s *PriorityClass) bindKeys(aa *ui.KeyActions) {\n\taa.Add(ui.KeyU, ui.NewKeyAction(\"UsedBy\", s.refCmd, true))\n}\n\nfunc (s *PriorityClass) refCmd(evt *tcell.EventKey) *tcell.EventKey {\n\treturn scanRefs(evt, s.App(), s.GetTable(), client.PcGVR)\n}\n"
  },
  {
    "path": "internal/view/priorityclass_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPriorityClassNew(t *testing.T) {\n\ts := view.NewPriorityClass(client.PcGVR)\n\n\trequire.NoError(t, s.Init(makeCtx(t)))\n\tassert.Equal(t, \"PriorityClass\", s.Name())\n\tassert.Len(t, s.Hints(), 8)\n}\n"
  },
  {
    "path": "internal/view/pulse.go",
    "content": "package view\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"image\"\n\t\"log/slog\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/tchart\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n)\n\nconst (\n\tcpuFmt     = \" %s [%s::b]%s[white::-]([%s::]%sm[white::]/[%s::]%sm[-::])\"\n\tmemFmt     = \" %s [%s::b]%s[white::-]([%s::]%sMi[white::]/[%s::]%sMi[-::])\"\n\tpulseTitle = \"Pulses\"\n\tNSTitleFmt = \"[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] \"\n\tdirLeft    = 1\n\tdirRight   = -dirLeft\n\tdirDown    = 4\n\tdirUp      = -dirDown\n\tgrayC      = \"gray\"\n)\n\nvar corpusGVRs = append(model.PulseGVRs, client.CpuGVR, client.MemGVR)\n\ntype Charts map[*client.GVR]Graphable\n\n// Graphable represents a graphic component.\ntype Graphable interface {\n\ttview.Primitive\n\n\t// ID returns the graph id.\n\tID() string\n\n\t// Add adds a metric\n\tAdd(ok, fault int)\n\n\tAddMetric(time.Time, float64)\n\n\t// SetLegend sets the graph legend\n\tSetLegend(string)\n\n\tSetColorIndex(int)\n\n\tSetMax(float64)\n\tGetMax() float64\n\n\t// SetSeriesColors sets charts series colors.\n\tSetSeriesColors(...tcell.Color)\n\n\t// GetSeriesColorNames returns the series color names.\n\tGetSeriesColorNames() []string\n\n\t// SetFocusColorNames sets the focus color names.\n\tSetFocusColorNames(fg, bg string)\n\n\t// SetBackgroundColor sets chart bg color.\n\tSetBackgroundColor(tcell.Color)\n\n\tSetBorderColor(tcell.Color) *tview.Box\n\n\t// IsDial returns true if chart is a dial\n\tIsDial() bool\n}\n\n// Pulse represents a command health view.\ntype Pulse struct {\n\t*tview.Grid\n\n\tapp            *App\n\tgvr            *client.GVR\n\tmodel          *model.Pulse\n\tcancelFn       context.CancelFunc\n\tactions        *ui.KeyActions\n\tcharts         Charts\n\tprevFocusIndex int\n\tchartGVRs      client.GVRs\n}\n\n// NewPulse returns a new alias view.\nfunc NewPulse(gvr *client.GVR) ResourceViewer {\n\treturn &Pulse{\n\t\tGrid:           tview.NewGrid(),\n\t\tmodel:          model.NewPulse(gvr),\n\t\tactions:        ui.NewKeyActions(),\n\t\tprevFocusIndex: -1,\n\t}\n}\n\n// Init initializes the view.\nfunc (p *Pulse) Init(ctx context.Context) error {\n\tp.SetBorder(true)\n\tp.SetGap(0, 0)\n\tp.SetBorderPadding(0, 0, 1, 1)\n\tvar err error\n\tif p.app, err = extractApp(ctx); err != nil {\n\t\treturn err\n\t}\n\n\tns := p.app.Config.ActiveNamespace()\n\tframe := p.app.Styles.Frame()\n\tp.SetTitle(ui.SkinTitle(fmt.Sprintf(NSTitleFmt, pulseTitle, ns), &frame))\n\n\tindex, chartRow := 4, 6\n\tif client.IsAllNamespace(ns) {\n\t\tindex, chartRow = 0, 8\n\t}\n\tp.chartGVRs = corpusGVRs[index:]\n\n\tp.charts = make(Charts, len(p.chartGVRs))\n\tvar x, y, col int\n\tfor _, gvr := range p.chartGVRs[:len(p.chartGVRs)-2] {\n\t\tp.charts[gvr] = p.makeGA(image.Point{X: x, Y: y}, image.Point{X: 2, Y: 2}, gvr)\n\t\tcol, y = col+1, y+2\n\t\tif y > 6 {\n\t\t\ty = 0\n\t\t}\n\t\tif col >= 4 {\n\t\t\tcol, x = 0, x+2\n\t\t}\n\t}\n\tif p.app.Conn().HasMetrics() {\n\t\tp.charts[client.CpuGVR] = p.makeSP(image.Point{X: chartRow, Y: 0}, image.Point{X: 2, Y: 4}, client.CpuGVR, \"c\")\n\t\tp.charts[client.MemGVR] = p.makeSP(image.Point{X: chartRow, Y: 4}, image.Point{X: 2, Y: 4}, client.MemGVR, \"Gi\")\n\t}\n\tp.GetItem(0).Focus = true\n\tp.app.SetFocus(p.charts[p.chartGVRs[0]])\n\n\tp.bindKeys()\n\tp.app.Styles.AddListener(p)\n\tp.StylesChanged(p.app.Styles)\n\tp.model.SetNamespace(ns)\n\n\treturn nil\n}\n\n// InCmdMode checks if prompt is active.\nfunc (*Pulse) InCmdMode() bool {\n\treturn false\n}\n\nfunc (*Pulse) SetCommand(*cmd.Interpreter)            {}\nfunc (*Pulse) SetFilter(string, bool)                 {}\nfunc (*Pulse) SetLabelSelector(labels.Selector, bool) {}\n\n// StylesChanged notifies the skin changed.\nfunc (p *Pulse) StylesChanged(s *config.Styles) {\n\tp.SetBackgroundColor(s.Charts().BgColor.Color())\n\tfor _, c := range p.charts {\n\t\tc.SetFocusColorNames(s.Charts().FocusFgColor.String(), s.Charts().FocusBgColor.String())\n\t\tif c.IsDial() {\n\t\t\tc.SetBackgroundColor(s.Charts().DialBgColor.Color())\n\t\t\tc.SetSeriesColors(s.Charts().DefaultDialColors.Colors()...)\n\t\t} else {\n\t\t\tc.SetBackgroundColor(s.Charts().ChartBgColor.Color())\n\t\t\tc.SetSeriesColors(s.Charts().DefaultChartColors.Colors()...)\n\t\t}\n\t\tif ss, ok := s.Charts().ResourceColors[c.ID()]; ok {\n\t\t\tc.SetSeriesColors(ss.Colors()...)\n\t\t}\n\t}\n}\n\n// SeriesChanged update cluster time series.\nfunc (p *Pulse) SeriesChanged(tt dao.TimeSeries) {\n\tif len(tt) == 0 {\n\t\treturn\n\t}\n\n\tcpu, ok := p.charts[client.CpuGVR]\n\tif !ok {\n\t\treturn\n\t}\n\tmem := p.charts[client.MemGVR]\n\tif !ok {\n\t\treturn\n\t}\n\n\tfor i := range tt {\n\t\tt := tt[i]\n\t\tcpu.SetMax(float64(t.Value.AllocatableCPU))\n\t\tmem.SetMax(float64(t.Value.AllocatableMEM))\n\t\tcpu.AddMetric(t.Time, float64(t.Value.CurrentCPU))\n\t\tmem.AddMetric(t.Time, float64(t.Value.CurrentMEM))\n\t}\n\n\tlast := tt[len(tt)-1]\n\tperc := client.ToPercentage(last.Value.CurrentCPU, int64(cpu.GetMax()))\n\tindex := int(p.app.Config.K9s.Thresholds.LevelFor(\"cpu\", perc))\n\tcpu.SetColorIndex(int(p.app.Config.K9s.Thresholds.LevelFor(\"cpu\", perc)))\n\tnn := cpu.GetSeriesColorNames()\n\tif last.Value.CurrentCPU == 0 {\n\t\tnn[0] = grayC\n\t}\n\tif last.Value.AllocatableCPU == 0 {\n\t\tnn[1] = grayC\n\t}\n\tcpu.SetLegend(fmt.Sprintf(cpuFmt,\n\t\tcases.Title(language.English).String(client.CpuGVR.R()),\n\t\tp.app.Config.K9s.Thresholds.SeverityColor(\"cpu\", perc),\n\t\trender.PrintPerc(perc),\n\t\tnn[index],\n\t\trender.AsThousands(last.Value.CurrentCPU),\n\t\t\"white\",\n\t\trender.AsThousands(int64(cpu.GetMax())),\n\t))\n\n\tnn = mem.GetSeriesColorNames()\n\tif last.Value.CurrentMEM == 0 {\n\t\tnn[0] = grayC\n\t}\n\tif last.Value.AllocatableMEM == 0 {\n\t\tnn[1] = grayC\n\t}\n\tperc = client.ToPercentage(last.Value.CurrentMEM, int64(mem.GetMax()))\n\tindex = int(p.app.Config.K9s.Thresholds.LevelFor(\"memory\", perc))\n\tmem.SetColorIndex(index)\n\tmem.SetLegend(fmt.Sprintf(memFmt,\n\t\tcases.Title(language.English).String(client.MemGVR.R()),\n\t\tp.app.Config.K9s.Thresholds.SeverityColor(\"memory\", perc),\n\t\trender.PrintPerc(perc),\n\t\tnn[index],\n\t\trender.AsThousands(last.Value.CurrentMEM),\n\t\t\"white\",\n\t\trender.AsThousands(int64(mem.GetMax())),\n\t))\n}\n\n// PulseChanged notifies the model data changed.\nfunc (p *Pulse) PulseChanged(pt model.HealthPoint) {\n\tv, ok := p.charts[pt.GVR]\n\tif !ok {\n\t\treturn\n\t}\n\n\tnn := v.GetSeriesColorNames()\n\tif pt.Total == 0 {\n\t\tnn[0] = grayC\n\t}\n\tif pt.Faults == 0 {\n\t\tnn[1] = grayC\n\t}\n\n\tv.SetLegend(cases.Title(language.English).String(pt.GVR.R()))\n\tif pt.Faults > 0 {\n\t\tv.SetBorderColor(tcell.ColorDarkRed)\n\t} else {\n\t\tv.SetBorderColor(tcell.ColorDarkOliveGreen)\n\t}\n\tv.Add(pt.Total, pt.Faults)\n}\n\n// PulseFailed notifies the load failed.\nfunc (p *Pulse) PulseFailed(err error) {\n\tp.app.Flash().Err(err)\n}\n\nfunc (p *Pulse) bindKeys() {\n\tp.actions.Merge(ui.NewKeyActionsFromMap(ui.KeyMap{\n\t\ttcell.KeyEnter:   ui.NewKeyAction(\"Goto\", p.enterCmd, true),\n\t\ttcell.KeyTab:     ui.NewKeyAction(\"Next\", p.nextFocusCmd(dirLeft), true),\n\t\ttcell.KeyBacktab: ui.NewKeyAction(\"Prev\", p.nextFocusCmd(dirRight), true),\n\t\ttcell.KeyDown:    ui.NewKeyAction(\"Down\", p.nextFocusCmd(dirDown), false),\n\t\ttcell.KeyUp:      ui.NewKeyAction(\"Up\", p.nextFocusCmd(dirUp), false),\n\t\ttcell.KeyRight:   ui.NewKeyAction(\"Next\", p.nextFocusCmd(dirLeft), false),\n\t\ttcell.KeyLeft:    ui.NewKeyAction(\"Prev\", p.nextFocusCmd(dirRight), false),\n\t\tui.KeyH:          ui.NewKeyAction(\"Prev\", p.nextFocusCmd(dirRight), false),\n\t\tui.KeyJ:          ui.NewKeyAction(\"Down\", p.nextFocusCmd(dirDown), false),\n\t\tui.KeyK:          ui.NewKeyAction(\"Up\", p.nextFocusCmd(dirUp), false),\n\t\tui.KeyL:          ui.NewKeyAction(\"Next\", p.nextFocusCmd(dirLeft), false),\n\t}))\n}\n\nfunc (p *Pulse) keyboard(evt *tcell.EventKey) *tcell.EventKey {\n\tkey := evt.Key()\n\tif key == tcell.KeyRune {\n\t\tkey = tcell.Key(evt.Rune())\n\t}\n\tif a, ok := p.actions.Get(key); ok {\n\t\treturn a.Action(evt)\n\t}\n\n\treturn evt\n}\n\nfunc (p *Pulse) defaultContext() context.Context {\n\treturn context.WithValue(context.Background(), internal.KeyFactory, p.app.factory)\n}\n\nfunc (*Pulse) Restart() {}\n\n// Start initializes resource watch loop.\nfunc (p *Pulse) Start() {\n\tp.Stop()\n\n\tctx := p.defaultContext()\n\tctx, p.cancelFn = context.WithCancel(ctx)\n\tgaugeChan, metricsChan, err := p.model.Watch(ctx)\n\tif err != nil {\n\t\tslog.Error(\"Pulse watch failed\", slogs.Error, err)\n\t\treturn\n\t}\n\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase check, ok := <-gaugeChan:\n\t\t\t\tif !ok {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tp.app.QueueUpdateDraw(func() {\n\t\t\t\t\tp.PulseChanged(check)\n\t\t\t\t})\n\t\t\tcase mx, ok := <-metricsChan:\n\t\t\t\tif !ok {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tp.app.QueueUpdateDraw(func() {\n\t\t\t\t\tp.SeriesChanged(mx)\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}()\n}\n\n// Stop terminates watch loop.\nfunc (p *Pulse) Stop() {\n\tif p.cancelFn == nil {\n\t\treturn\n\t}\n\tp.cancelFn()\n\tp.cancelFn = nil\n}\n\n// Refresh updates the view\nfunc (*Pulse) Refresh() {}\n\n// GVR returns a resource descriptor.\nfunc (p *Pulse) GVR() *client.GVR {\n\treturn p.gvr\n}\n\n// Name returns the component name.\nfunc (*Pulse) Name() string {\n\treturn pulseTitle\n}\n\n// App returns the current app handle.\nfunc (p *Pulse) App() *App {\n\treturn p.app\n}\n\n// SetInstance sets specific resource instance.\nfunc (*Pulse) SetInstance(string) {}\n\n// SetEnvFn sets the custom environment function.\nfunc (*Pulse) SetEnvFn(EnvFunc) {}\n\n// AddBindKeysFn sets up extra key bindings.\nfunc (*Pulse) AddBindKeysFn(BindKeysFunc) {}\n\n// SetContextFn sets custom context.\nfunc (*Pulse) SetContextFn(ContextFunc) {}\n\nfunc (*Pulse) GetContextFn() ContextFunc { return nil }\n\n// GetTable return the view table if any.\nfunc (*Pulse) GetTable() *Table {\n\treturn nil\n}\n\n// Actions returns active menu bindings.\nfunc (p *Pulse) Actions() *ui.KeyActions {\n\treturn p.actions\n}\n\n// Hints returns the view hints.\nfunc (p *Pulse) Hints() model.MenuHints {\n\treturn p.actions.Hints()\n}\n\n// ExtraHints returns additional hints.\nfunc (*Pulse) ExtraHints() map[string]string {\n\treturn nil\n}\n\nfunc (p *Pulse) enterCmd(*tcell.EventKey) *tcell.EventKey {\n\tv := p.App().GetFocus()\n\ts, ok := v.(Graphable)\n\tif !ok {\n\t\treturn nil\n\t}\n\tg, ok := v.(Graphable)\n\tif !ok {\n\t\treturn nil\n\t}\n\tp.prevFocusIndex = p.findIndex(g)\n\tfor i := range len(p.charts) {\n\t\tgi := p.GetItem(i)\n\t\tif i == p.prevFocusIndex {\n\t\t\tgi.Focus = true\n\t\t} else {\n\t\t\tgi.Focus = false\n\t\t}\n\t}\n\n\tp.Stop()\n\tres := client.NewGVR(s.ID()).R()\n\tif res == \"cpu\" || res == \"memory\" {\n\t\tres = client.PodGVR.String()\n\t}\n\tp.App().SetFocus(p.App().Main)\n\tp.App().gotoResource(res+\" \"+p.model.GetNamespace(), \"\", false, true)\n\n\treturn nil\n}\n\nfunc (p *Pulse) nextFocusCmd(direction int) func(evt *tcell.EventKey) *tcell.EventKey {\n\treturn func(*tcell.EventKey) *tcell.EventKey {\n\t\tv := p.app.GetFocus()\n\t\tg, ok := v.(Graphable)\n\t\tif !ok {\n\t\t\treturn nil\n\t\t}\n\n\t\tcurrentIndex := p.findIndex(g)\n\t\tnextIndex, total := currentIndex+direction, len(p.charts)\n\t\tif nextIndex < 0 {\n\t\t\treturn nil\n\t\t}\n\n\t\tswitch direction {\n\t\tcase dirLeft:\n\t\t\tif nextIndex >= total {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tp.prevFocusIndex = -1\n\t\tcase dirRight:\n\t\t\tp.prevFocusIndex = -1\n\t\tcase dirUp:\n\t\t\tif p.app.Conn().HasMetrics() {\n\t\t\t\tif currentIndex >= total-2 {\n\t\t\t\t\tif p.prevFocusIndex >= 0 && p.prevFocusIndex != currentIndex {\n\t\t\t\t\t\tnextIndex = p.prevFocusIndex\n\t\t\t\t\t} else if currentIndex == p.chartGVRs.Len()-1 {\n\t\t\t\t\t\tnextIndex += 1\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tp.prevFocusIndex = currentIndex\n\t\t\t\t}\n\t\t\t}\n\t\tcase dirDown:\n\t\t\tif p.app.Conn().HasMetrics() {\n\t\t\t\tif currentIndex >= total-6 && currentIndex < total-2 {\n\t\t\t\t\tswitch {\n\t\t\t\t\tcase (currentIndex % 4) <= 1:\n\t\t\t\t\t\tp.prevFocusIndex, nextIndex = currentIndex, total-2\n\t\t\t\t\tcase (currentIndex % 4) <= 3:\n\t\t\t\t\t\tp.prevFocusIndex, nextIndex = currentIndex, total-1\n\t\t\t\t\t}\n\t\t\t\t} else if currentIndex >= total-2 {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif nextIndex < 0 {\n\t\t\tnextIndex = 0\n\t\t} else if nextIndex > total-1 {\n\t\t\tnextIndex = currentIndex\n\t\t}\n\t\tp.GetItem(nextIndex).Focus = false\n\t\tp.GetItem(nextIndex).Item.Blur()\n\t\ti, v := p.nextFocus(nextIndex)\n\t\tp.GetItem(i).Focus = true\n\t\tp.app.SetFocus(v)\n\n\t\treturn nil\n\t}\n}\n\nfunc (p *Pulse) makeSP(loc, span image.Point, gvr *client.GVR, unit string) *tchart.SparkLine {\n\ts := tchart.NewSparkLine(gvr.String(), unit)\n\ts.SetBackgroundColor(p.app.Styles.Charts().BgColor.Color())\n\tif cc, ok := p.app.Styles.Charts().ResourceColors[gvr.String()]; ok {\n\t\ts.SetSeriesColors(cc.Colors()...)\n\t} else {\n\t\ts.SetSeriesColors(p.app.Styles.Charts().DefaultChartColors.Colors()...)\n\t}\n\ts.SetLegend(fmt.Sprintf(\" %s \", cases.Title(language.English).String(gvr.R())))\n\ts.SetInputCapture(p.keyboard)\n\tp.AddItem(s, loc.X, loc.Y, span.X, span.Y, 0, 0, false)\n\n\treturn s\n}\n\nfunc (p *Pulse) makeGA(loc, span image.Point, gvr *client.GVR) *tchart.Gauge {\n\tg := tchart.NewGauge(gvr.String())\n\tg.SetBorder(true)\n\tg.SetBackgroundColor(p.app.Styles.Charts().BgColor.Color())\n\tif cc, ok := p.app.Styles.Charts().ResourceColors[gvr.String()]; ok {\n\t\tg.SetSeriesColors(cc.Colors()...)\n\t} else {\n\t\tg.SetSeriesColors(p.app.Styles.Charts().DefaultDialColors.Colors()...)\n\t}\n\tg.SetLegend(fmt.Sprintf(\" %s \", cases.Title(language.English).String(gvr.R())))\n\tg.SetInputCapture(p.keyboard)\n\tp.AddItem(g, loc.X, loc.Y, span.X, span.Y, 0, 0, false)\n\n\treturn g\n}\n\n// ----------------------------------------------------------------------------\n// Helpers\n\nfunc (p *Pulse) nextFocus(index int) (int, tview.Primitive) {\n\tif index >= len(p.chartGVRs) {\n\t\treturn 0, p.charts[p.chartGVRs[0]]\n\t}\n\n\tif index < 0 {\n\t\treturn len(p.chartGVRs) - 1, p.charts[p.chartGVRs[len(p.chartGVRs)-1]]\n\t}\n\n\treturn index, p.charts[p.chartGVRs[index]]\n}\n\nfunc (p *Pulse) findIndex(g Graphable) int {\n\tfor i, gvr := range p.chartGVRs {\n\t\tif gvr.String() == g.ID() {\n\t\t\treturn i\n\t\t}\n\t}\n\treturn 0\n}\n"
  },
  {
    "path": "internal/view/pvc.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\n// PersistentVolumeClaim represents a PVC custom viewer.\ntype PersistentVolumeClaim struct {\n\tResourceViewer\n}\n\n// NewPersistentVolumeClaim returns a new viewer.\nfunc NewPersistentVolumeClaim(gvr *client.GVR) ResourceViewer {\n\tv := PersistentVolumeClaim{\n\t\tResourceViewer: NewOwnerExtender(NewBrowser(gvr)),\n\t}\n\tv.AddBindKeysFn(v.bindKeys)\n\n\treturn &v\n}\n\nfunc (p *PersistentVolumeClaim) bindKeys(aa *ui.KeyActions) {\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyU: ui.NewKeyAction(\"UsedBy\", p.refCmd, true),\n\t})\n}\n\nfunc (p *PersistentVolumeClaim) refCmd(evt *tcell.EventKey) *tcell.EventKey {\n\treturn scanRefs(evt, p.App(), p.GetTable(), client.PvcGVR)\n}\n"
  },
  {
    "path": "internal/view/pvc_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPVCNew(t *testing.T) {\n\tv := view.NewPersistentVolumeClaim(client.PvcGVR)\n\n\trequire.NoError(t, v.Init(makeCtx(t)))\n\tassert.Equal(t, \"PersistentVolumeClaims\", v.Name())\n\tassert.Len(t, v.Hints(), 9)\n}\n"
  },
  {
    "path": "internal/view/rbac.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\n// Rbac presents an RBAC policy viewer.\ntype Rbac struct {\n\tResourceViewer\n}\n\n// NewRbac returns a new viewer.\nfunc NewRbac(gvr *client.GVR) ResourceViewer {\n\tr := Rbac{\n\t\tResourceViewer: NewBrowser(gvr),\n\t}\n\tr.AddBindKeysFn(r.bindKeys)\n\tr.GetTable().SetSortCol(\"API-GROUP\", true)\n\tr.GetTable().SetEnterFn(blankEnterFn)\n\n\treturn &r\n}\n\nfunc (*Rbac) bindKeys(aa *ui.KeyActions) {\n\taa.Delete(ui.KeyShiftA, tcell.KeyCtrlSpace, ui.KeySpace)\n}\n\nfunc showRules(app *App, _ ui.Tabular, gvr *client.GVR, path string) {\n\tv := NewRbac(client.RbacGVR)\n\tv.SetContextFn(rbacCtx(gvr, path))\n\n\tif err := app.inject(v, false); err != nil {\n\t\tapp.Flash().Err(err)\n\t}\n}\n\nfunc rbacCtx(gvr *client.GVR, path string) ContextFunc {\n\treturn func(ctx context.Context) context.Context {\n\t\tctx = context.WithValue(ctx, internal.KeyPath, path)\n\t\treturn context.WithValue(ctx, internal.KeyGVR, gvr)\n\t}\n}\n\nfunc blankEnterFn(_ *App, _ ui.Tabular, _ *client.GVR, _ string) {}\n"
  },
  {
    "path": "internal/view/rbac_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRbacNew(t *testing.T) {\n\tv := view.NewRbac(client.RbacGVR)\n\n\trequire.NoError(t, v.Init(makeCtx(t)))\n\tassert.Equal(t, \"Rbac\", v.Name())\n\tassert.Len(t, v.Hints(), 6)\n}\n"
  },
  {
    "path": "internal/view/reference.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\n// Reference represents resource references.\ntype Reference struct {\n\tResourceViewer\n}\n\n// NewReference returns a new alias view.\nfunc NewReference(gvr *client.GVR) ResourceViewer {\n\tr := Reference{\n\t\tResourceViewer: NewBrowser(gvr),\n\t}\n\tr.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen)\n\tr.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorMediumSpringGreen).Attributes(tcell.AttrNone))\n\tr.AddBindKeysFn(r.bindKeys)\n\n\treturn &r\n}\n\n// Init initializes the view.\nfunc (r *Reference) Init(ctx context.Context) error {\n\tif err := r.ResourceViewer.Init(ctx); err != nil {\n\t\treturn err\n\t}\n\tr.GetTable().GetModel().SetNamespace(client.BlankNamespace)\n\n\treturn nil\n}\n\nfunc (r *Reference) bindKeys(aa *ui.KeyActions) {\n\taa.Delete(ui.KeyShiftA, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace)\n\taa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL, tcell.KeyCtrlZ)\n\taa.Bulk(ui.KeyMap{\n\t\ttcell.KeyEnter: ui.NewKeyAction(\"Goto\", r.gotoCmd, true),\n\t\tui.KeyShiftV:   ui.NewKeyAction(\"Sort GVR\", r.GetTable().SortColCmd(\"GVR\", true), false),\n\t})\n}\n\nfunc (r *Reference) gotoCmd(evt *tcell.EventKey) *tcell.EventKey {\n\trow, _ := r.GetTable().GetSelection()\n\tif row == 0 {\n\t\treturn evt\n\t}\n\n\tpath := r.GetTable().GetSelectedItem()\n\tns, _ := client.Namespaced(path)\n\tgvr := ui.TrimCell(r.GetTable().SelectTable, row, 2)\n\tr.App().gotoResource(client.NewGVR(gvr).String()+\" \"+ns, path, false, true)\n\n\treturn evt\n}\n"
  },
  {
    "path": "internal/view/reference_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestReferenceNew(t *testing.T) {\n\ts := view.NewReference(client.RefGVR)\n\n\trequire.NoError(t, s.Init(makeCtx(t)))\n\tassert.Equal(t, \"References\", s.Name())\n\tassert.Len(t, s.Hints(), 6)\n}\n"
  },
  {
    "path": "internal/view/registrar.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"github.com/derailed/k9s/internal/client\"\n)\n\nfunc loadCustomViewers() MetaViewers {\n\tm := make(MetaViewers, 30)\n\tcoreViewers(m)\n\tmiscViewers(m)\n\tappsViewers(m)\n\trbacViewers(m)\n\tbatchViewers(m)\n\tcrdViewers(m)\n\thelmViewers(m)\n\n\treturn m\n}\n\nfunc helmViewers(vv MetaViewers) {\n\tvv[client.HmGVR] = MetaViewer{\n\t\tviewerFn: NewHelmChart,\n\t}\n}\n\nfunc coreViewers(vv MetaViewers) {\n\tvv[client.NsGVR] = MetaViewer{\n\t\tviewerFn: NewNamespace,\n\t}\n\tvv[client.EvGVR] = MetaViewer{\n\t\tviewerFn: NewEvent,\n\t}\n\tvv[client.PodGVR] = MetaViewer{\n\t\tviewerFn: NewPod,\n\t}\n\tvv[client.SvcGVR] = MetaViewer{\n\t\tviewerFn: NewService,\n\t}\n\tvv[client.NodeGVR] = MetaViewer{\n\t\tviewerFn: NewNode,\n\t}\n\tvv[client.SecGVR] = MetaViewer{\n\t\tviewerFn: NewSecret,\n\t}\n\tvv[client.PcGVR] = MetaViewer{\n\t\tviewerFn: NewPriorityClass,\n\t}\n\tvv[client.CmGVR] = MetaViewer{\n\t\tviewerFn: NewConfigMap,\n\t}\n\tvv[client.SaGVR] = MetaViewer{\n\t\tviewerFn: NewServiceAccount,\n\t}\n\tvv[client.PvcGVR] = MetaViewer{\n\t\tviewerFn: NewPersistentVolumeClaim,\n\t}\n}\n\nfunc miscViewers(vv MetaViewers) {\n\tvv[client.WkGVR] = MetaViewer{\n\t\tviewerFn: NewWorkload,\n\t}\n\tvv[client.CtGVR] = MetaViewer{\n\t\tviewerFn: NewContext,\n\t}\n\tvv[client.CoGVR] = MetaViewer{\n\t\tviewerFn: NewContainer,\n\t}\n\tvv[client.ScnGVR] = MetaViewer{\n\t\tviewerFn: NewImageScan,\n\t}\n\tvv[client.PfGVR] = MetaViewer{\n\t\tviewerFn: NewPortForward,\n\t}\n\tvv[client.SdGVR] = MetaViewer{\n\t\tviewerFn: NewScreenDump,\n\t}\n\tvv[client.BeGVR] = MetaViewer{\n\t\tviewerFn: NewBenchmark,\n\t}\n\tvv[client.AliGVR] = MetaViewer{\n\t\tviewerFn: NewAlias,\n\t}\n\tvv[client.RefGVR] = MetaViewer{\n\t\tviewerFn: NewReference,\n\t}\n\tvv[client.PuGVR] = MetaViewer{\n\t\tviewerFn: NewPulse,\n\t}\n}\n\nfunc appsViewers(vv MetaViewers) {\n\tvv[client.DpGVR] = MetaViewer{\n\t\tviewerFn: NewDeploy,\n\t}\n\tvv[client.RsGVR] = MetaViewer{\n\t\tviewerFn: NewReplicaSet,\n\t}\n\tvv[client.StsGVR] = MetaViewer{\n\t\tviewerFn: NewStatefulSet,\n\t}\n\tvv[client.DsGVR] = MetaViewer{\n\t\tviewerFn: NewDaemonSet,\n\t}\n}\n\nfunc rbacViewers(vv MetaViewers) {\n\tvv[client.RbacGVR] = MetaViewer{\n\t\tenterFn: showRules,\n\t}\n\tvv[client.UsrGVR] = MetaViewer{\n\t\tviewerFn: NewUser,\n\t}\n\tvv[client.GrpGVR] = MetaViewer{\n\t\tviewerFn: NewGroup,\n\t}\n\tvv[client.CrGVR] = MetaViewer{\n\t\tenterFn: showRules,\n\t}\n\tvv[client.CrbGVR] = MetaViewer{\n\t\tenterFn: showRules,\n\t}\n\tvv[client.RoGVR] = MetaViewer{\n\t\tenterFn: showRules,\n\t}\n\tvv[client.RobGVR] = MetaViewer{\n\t\tenterFn: showRules,\n\t}\n}\n\nfunc batchViewers(vv MetaViewers) {\n\tvv[client.CjGVR] = MetaViewer{\n\t\tviewerFn: NewCronJob,\n\t}\n\tvv[client.JobGVR] = MetaViewer{\n\t\tviewerFn: NewJob,\n\t}\n}\n\nfunc crdViewers(vv MetaViewers) {\n\tvv[client.CrdGVR] = MetaViewer{\n\t\tviewerFn: NewCRD,\n\t}\n}\n"
  },
  {
    "path": "internal/view/restart_extender.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/ui/dialog\"\n\t\"github.com/derailed/tcell/v2\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// RestartExtender represents a restartable resource.\ntype RestartExtender struct {\n\tResourceViewer\n}\n\n// NewRestartExtender returns a new extender.\nfunc NewRestartExtender(v ResourceViewer) ResourceViewer {\n\tr := RestartExtender{ResourceViewer: v}\n\tv.AddBindKeysFn(r.bindKeys)\n\n\treturn &r\n}\n\n// BindKeys creates additional menu actions.\nfunc (r *RestartExtender) bindKeys(aa *ui.KeyActions) {\n\tif r.App().Config.IsReadOnly() {\n\t\treturn\n\t}\n\taa.Add(ui.KeyR, ui.NewKeyActionWithOpts(\"Restart\", r.restartCmd,\n\t\tui.ActionOpts{\n\t\t\tVisible:   true,\n\t\t\tDangerous: true,\n\t\t},\n\t))\n}\n\nfunc (r *RestartExtender) restartCmd(*tcell.EventKey) *tcell.EventKey {\n\tpaths := r.GetTable().GetSelectedItems()\n\tif len(paths) == 0 || paths[0] == \"\" {\n\t\treturn nil\n\t}\n\n\tr.Stop()\n\tdefer r.Start()\n\tmsg := fmt.Sprintf(\"Restart %s %s?\", singularize(r.GVR().R()), paths[0])\n\tif len(paths) > 1 {\n\t\tmsg = fmt.Sprintf(\"Restart %d %s?\", len(paths), r.GVR().R())\n\t}\n\td := r.App().Styles.Dialog()\n\n\topts := dialog.RestartDialogOpts{\n\t\tTitle:        \"Confirm Restart\",\n\t\tMessage:      msg,\n\t\tFieldManager: \"kubectl-rollout\",\n\t\tAck: func(opts *metav1.PatchOptions) bool {\n\t\t\tctx, cancel := context.WithTimeout(context.Background(), r.App().Conn().Config().CallTimeout())\n\t\t\tdefer cancel()\n\t\t\tfor _, path := range paths {\n\t\t\t\tif err := r.restartRollout(ctx, path, opts); err != nil {\n\t\t\t\t\tr.App().Flash().Err(err)\n\t\t\t\t} else {\n\t\t\t\t\tr.App().Flash().Infof(\"Restart in progress for `%s...\", path)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn true\n\t\t},\n\t\tCancel: func() {},\n\t}\n\tdialog.ShowRestart(&d, r.App().Content.Pages, &opts)\n\n\treturn nil\n}\n\nfunc (r *RestartExtender) restartRollout(ctx context.Context, path string, opts *metav1.PatchOptions) error {\n\tres, err := dao.AccessorFor(r.App().factory, r.GVR())\n\tif err != nil {\n\t\treturn err\n\t}\n\ts, ok := res.(dao.Restartable)\n\tif !ok {\n\t\treturn errors.New(\"resource is not restartable\")\n\t}\n\n\treturn s.Restart(ctx, path, opts)\n}\n\n// Helpers...\n\nfunc singularize(s string) string {\n\tif strings.LastIndex(s, \"s\") == len(s)-1 {\n\t\treturn s[:len(s)-1]\n\t}\n\n\treturn s\n}\n"
  },
  {
    "path": "internal/view/rs.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/ui/dialog\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\n// ReplicaSet presents a replicaset viewer.\ntype ReplicaSet struct {\n\tResourceViewer\n}\n\n// NewReplicaSet returns a new viewer.\nfunc NewReplicaSet(gvr *client.GVR) ResourceViewer {\n\tr := ReplicaSet{\n\t\tResourceViewer: NewOwnerExtender(\n\t\t\tNewVulnerabilityExtender(\n\t\t\t\tNewBrowser(gvr),\n\t\t\t),\n\t\t),\n\t}\n\tr.AddBindKeysFn(r.bindKeys)\n\tr.GetTable().SetEnterFn(r.showPods)\n\n\treturn &r\n}\n\nfunc (r *ReplicaSet) bindKeys(aa *ui.KeyActions) {\n\taa.Bulk(ui.KeyMap{\n\t\ttcell.KeyCtrlL: ui.NewKeyAction(\"Rollback\", r.rollbackCmd, true),\n\t})\n}\n\nfunc (*ReplicaSet) showPods(app *App, _ ui.Tabular, _ *client.GVR, path string) {\n\tvar drs dao.ReplicaSet\n\trs, err := drs.Load(app.factory, path)\n\tif err != nil {\n\t\tapp.Flash().Err(err)\n\t\treturn\n\t}\n\n\tshowPodsFromSelector(app, path, rs.Spec.Selector)\n}\n\nfunc (r *ReplicaSet) rollbackCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := r.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\n\tmsg := fmt.Sprintf(\"Rollback %s %s?\", r.GVR(), path)\n\n\td := r.App().Styles.Dialog()\n\tdialog.ShowConfirm(&d, r.App().Content.Pages, \"Rollback\", msg, func() {\n\t\tr.App().Flash().Infof(\"Rolling back %s %s\", r.GVR(), path)\n\t\tvar drs dao.ReplicaSet\n\t\tdrs.Init(r.App().factory, r.GVR())\n\t\tif err := drs.Rollback(path); err != nil {\n\t\t\tr.App().Flash().Err(err)\n\t\t} else {\n\t\t\tr.App().Flash().Infof(\"%s successfully rolled back\", path)\n\t\t}\n\t\tr.Refresh()\n\t}, func() {})\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/view/sa.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\n// ServiceAccount represents a serviceaccount viewer.\ntype ServiceAccount struct {\n\tResourceViewer\n}\n\n// NewServiceAccount returns a new viewer.\nfunc NewServiceAccount(gvr *client.GVR) ResourceViewer {\n\ts := ServiceAccount{\n\t\tResourceViewer: NewOwnerExtender(NewBrowser(gvr)),\n\t}\n\ts.AddBindKeysFn(s.bindKeys)\n\ts.SetContextFn(s.subjectCtx)\n\n\treturn &s\n}\n\nfunc (s *ServiceAccount) bindKeys(aa *ui.KeyActions) {\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyU:        ui.NewKeyAction(\"UsedBy\", s.refCmd, true),\n\t\ttcell.KeyEnter: ui.NewKeyAction(\"Rules\", s.policyCmd, true),\n\t})\n}\n\nfunc (*ServiceAccount) subjectCtx(ctx context.Context) context.Context {\n\treturn context.WithValue(ctx, internal.KeySubjectKind, sa)\n}\n\nfunc (s *ServiceAccount) refCmd(evt *tcell.EventKey) *tcell.EventKey {\n\treturn scanSARefs(evt, s.App(), s.GetTable(), client.SaGVR)\n}\n\nfunc (s *ServiceAccount) policyCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := s.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\tif err := s.App().inject(NewPolicy(s.App(), sa, path), false); err != nil {\n\t\ts.App().Flash().Err(err)\n\t}\n\n\treturn nil\n}\n\nfunc scanSARefs(evt *tcell.EventKey, a *App, t *Table, gvr *client.GVR) *tcell.EventKey {\n\tpath := t.GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\n\tctx := context.Background()\n\trefs, err := dao.ScanForSARefs(refContext(gvr, path, true)(ctx), a.factory)\n\tif err != nil {\n\t\ta.Flash().Err(err)\n\t\treturn nil\n\t}\n\tif len(refs) == 0 {\n\t\ta.Flash().Warnf(\"No references found at this time for %s::%s. Check again later!\", gvr, path)\n\t\treturn nil\n\t}\n\ta.Flash().Infof(\"Viewing references for %s::%s\", gvr, path)\n\tview := NewReference(client.RefGVR)\n\tview.SetContextFn(refContext(gvr, path, false))\n\tif err := a.inject(view, false); err != nil {\n\t\ta.Flash().Err(err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/view/scale_extender.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n)\n\n// ScaleExtender adds scaling extensions.\ntype ScaleExtender struct {\n\tResourceViewer\n}\n\n// NewScaleExtender returns a new extender.\nfunc NewScaleExtender(r ResourceViewer) ResourceViewer {\n\ts := ScaleExtender{ResourceViewer: r}\n\ts.AddBindKeysFn(s.bindKeys)\n\n\treturn &s\n}\n\nfunc (s *ScaleExtender) bindKeys(aa *ui.KeyActions) {\n\tif s.App().Config.IsReadOnly() {\n\t\treturn\n\t}\n\n\tmeta, err := dao.MetaAccess.MetaFor(s.GVR())\n\tif err != nil {\n\t\tslog.Error(\"No meta information found\",\n\t\t\tslogs.GVR, s.GVR(),\n\t\t\tslogs.Error, err,\n\t\t)\n\t\treturn\n\t}\n\n\tif dao.IsScalable(meta) {\n\t\taa.Add(ui.KeyS, ui.NewKeyActionWithOpts(\"Scale\", s.scaleCmd,\n\t\t\tui.ActionOpts{\n\t\t\t\tVisible:   true,\n\t\t\t\tDangerous: true,\n\t\t\t},\n\t\t))\n\t}\n}\n\nfunc (s *ScaleExtender) scaleCmd(*tcell.EventKey) *tcell.EventKey {\n\tpaths := s.GetTable().GetSelectedItems()\n\tif len(paths) == 0 {\n\t\treturn nil\n\t}\n\n\ts.Stop()\n\tdefer s.Start()\n\ts.showScaleDialog(paths)\n\n\treturn nil\n}\n\nfunc (s *ScaleExtender) showScaleDialog(paths []string) {\n\tform, err := s.makeScaleForm(paths)\n\tif err != nil {\n\t\ts.App().Flash().Err(err)\n\t\treturn\n\t}\n\tconfirm := tview.NewModalForm(\"<Scale>\", form)\n\tmsg := fmt.Sprintf(\"Scale %s %s?\", singularize(s.GVR().R()), paths[0])\n\tif len(paths) > 1 {\n\t\tmsg = fmt.Sprintf(\"Scale [%d] %s?\", len(paths), s.GVR().R())\n\t}\n\tconfirm.SetText(msg)\n\tconfirm.SetDoneFunc(func(int, string) {\n\t\ts.dismissDialog()\n\t})\n\ts.App().Content.AddPage(scaleDialogKey, confirm, false, false)\n\ts.App().Content.ShowPage(scaleDialogKey)\n}\n\nfunc (s *ScaleExtender) valueOf(col string) (string, error) {\n\tcolIdx, ok := s.GetTable().HeaderIndex(col)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"no column index for %s\", col)\n\t}\n\treturn s.GetTable().GetSelectedCell(colIdx), nil\n}\n\nfunc (s *ScaleExtender) replicasFromReady(_ string) (string, error) {\n\treplicas, err := s.valueOf(\"READY\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\ttokens := strings.Split(replicas, \"/\")\n\tif len(tokens) < 2 {\n\t\treturn \"\", fmt.Errorf(\"unable to locate replicas from %s\", replicas)\n\t}\n\n\treturn strings.TrimRight(tokens[1], ui.DeltaSign), nil\n}\n\nfunc (s *ScaleExtender) replicasFromScaleSubresource(sel string) (string, error) {\n\tres, err := dao.AccessorFor(s.App().factory, s.GVR())\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treplicasGetter, ok := res.(dao.ReplicasGetter)\n\tif !ok {\n\t\treturn \"\", fmt.Errorf(\"expecting a replicasGetter resource for %q\", s.GVR())\n\t}\n\n\tctx, cancel := context.WithTimeout(context.Background(), s.App().Conn().Config().CallTimeout())\n\tdefer cancel()\n\n\treplicas, err := replicasGetter.Replicas(ctx, sel)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn strconv.Itoa(int(replicas)), nil\n}\n\nfunc (s *ScaleExtender) makeScaleForm(fqns []string) (*tview.Form, error) {\n\tfactor := \"0\"\n\tif len(fqns) == 1 {\n\t\t// If the CRD resource supports scaling, then first try to\n\t\t// read the replicas directly from the CRD.\n\t\tif meta, _ := dao.MetaAccess.MetaFor(s.GVR()); dao.IsScalable(meta) {\n\t\t\treplicas, err := s.replicasFromScaleSubresource(fqns[0])\n\t\t\tif err == nil && replicas != \"\" {\n\t\t\t\tfactor = replicas\n\t\t\t}\n\t\t}\n\n\t\t// For built-in resources or cases where we can't get the replicas from the CRD, we can\n\t\t// only try to get the number of copies from the READY field.\n\t\tif factor == \"0\" {\n\t\t\treplicas, err := s.replicasFromReady(fqns[0])\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tfactor = replicas\n\t\t}\n\t}\n\n\tstyles := s.App().Styles.Dialog()\n\tf := tview.NewForm().\n\t\tSetItemPadding(0).\n\t\tSetButtonsAlign(tview.AlignCenter).\n\t\tSetButtonBackgroundColor(styles.ButtonBgColor.Color()).\n\t\tSetButtonTextColor(styles.ButtonFgColor.Color()).\n\t\tSetLabelColor(styles.LabelFgColor.Color()).\n\t\tSetFieldTextColor(styles.FieldFgColor.Color())\n\n\tf.AddInputField(\"Replicas:\", factor, 4, func(textToCheck string, _ rune) bool {\n\t\t_, err := strconv.Atoi(textToCheck)\n\t\treturn err == nil\n\t}, func(changed string) {\n\t\tfactor = changed\n\t})\n\n\tf.AddButton(\"OK\", func() {\n\t\tdefer s.dismissDialog()\n\t\tcount, err := strconv.Atoi(factor)\n\t\tif err != nil {\n\t\t\ts.App().Flash().Err(err)\n\t\t\treturn\n\t\t}\n\t\tctx, cancel := context.WithTimeout(context.Background(), s.App().Conn().Config().CallTimeout())\n\t\tdefer cancel()\n\t\tfor _, fqn := range fqns {\n\t\t\tif err := s.scale(ctx, fqn, int32(count)); err != nil {\n\t\t\t\tslog.Error(\"Unable to scale resource\", slogs.FQN, fqn)\n\t\t\t\ts.App().Flash().Err(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tif len(fqns) != 1 {\n\t\t\ts.App().Flash().Infof(\"[%d] %s scaled successfully\", len(fqns), singularize(s.GVR().R()))\n\t\t} else {\n\t\t\ts.App().Flash().Infof(\"%s %s scaled successfully\", s.GVR().R(), fqns[0])\n\t\t}\n\t})\n\tf.AddButton(\"Cancel\", func() {\n\t\ts.dismissDialog()\n\t})\n\tfor i := range 2 {\n\t\tif b := f.GetButton(i); b != nil {\n\t\t\tb.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color())\n\t\t\tb.SetLabelColorActivated(styles.ButtonFocusFgColor.Color())\n\t\t}\n\t}\n\n\tfor i := range f.GetButtonCount() {\n\t\tf.GetButton(i).\n\t\t\tSetBackgroundColorActivated(styles.ButtonFocusBgColor.Color()).\n\t\t\tSetLabelColorActivated(styles.ButtonFocusFgColor.Color())\n\t}\n\n\treturn f, nil\n}\n\nfunc (s *ScaleExtender) dismissDialog() {\n\ts.App().Content.RemovePage(scaleDialogKey)\n}\n\nfunc (s *ScaleExtender) scale(ctx context.Context, path string, replicas int32) error {\n\tres, err := dao.AccessorFor(s.App().factory, s.GVR())\n\tif err != nil {\n\t\treturn err\n\t}\n\tscaler, ok := res.(dao.Scalable)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting a scalable resource for %q\", s.GVR())\n\t}\n\n\treturn scaler.Scale(ctx, path, replicas)\n}\n"
  },
  {
    "path": "internal/view/screen_dump.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\n// ScreenDump presents a directory listing viewer.\ntype ScreenDump struct {\n\tResourceViewer\n}\n\n// NewScreenDump returns a new viewer.\nfunc NewScreenDump(gvr *client.GVR) ResourceViewer {\n\ts := ScreenDump{\n\t\tResourceViewer: NewBrowser(gvr),\n\t}\n\ts.GetTable().SetBorderFocusColor(tcell.ColorSteelBlue)\n\ts.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorRoyalBlue).Attributes(tcell.AttrNone))\n\ts.GetTable().SetSortCol(ageCol, true)\n\ts.GetTable().SelectRow(1, 0, true)\n\ts.GetTable().SetEnterFn(s.edit)\n\ts.SetContextFn(s.dirContext)\n\n\treturn &s\n}\n\nfunc (s *ScreenDump) dirContext(ctx context.Context) context.Context {\n\tdir := s.App().Config.K9s.ContextScreenDumpDir()\n\tif err := data.EnsureFullPath(dir, data.DefaultDirMod); err != nil {\n\t\ts.App().Flash().Err(err)\n\t\treturn ctx\n\t}\n\n\treturn context.WithValue(ctx, internal.KeyDir, dir)\n}\n\nfunc (s *ScreenDump) edit(app *App, _ ui.Tabular, _ *client.GVR, path string) {\n\tslog.Debug(\"ScreenDump selection\", slogs.FQN, path)\n\n\ts.Stop()\n\tdefer s.Start()\n\tif !edit(app, &shellOpts{clear: true, args: []string{path}}) {\n\t\tapp.Flash().Errf(\"Failed to launch editor\")\n\t}\n}\n"
  },
  {
    "path": "internal/view/screen_dump_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestScreenDumpNew(t *testing.T) {\n\tpo := view.NewScreenDump(client.SdGVR)\n\n\trequire.NoError(t, po.Init(makeCtx(t)))\n\tassert.Equal(t, \"ScreenDumps\", po.Name())\n\tassert.Len(t, po.Hints(), 7)\n}\n"
  },
  {
    "path": "internal/view/secret.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n)\n\n// Secret presents a secret viewer.\ntype Secret struct {\n\tResourceViewer\n}\n\n// NewSecret returns a new viewer.\nfunc NewSecret(gvr *client.GVR) ResourceViewer {\n\ts := Secret{\n\t\tResourceViewer: NewOwnerExtender(NewBrowser(gvr)),\n\t}\n\ts.AddBindKeysFn(s.bindKeys)\n\n\treturn &s\n}\n\nfunc (s *Secret) bindKeys(aa *ui.KeyActions) {\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyX: ui.NewKeyAction(\"Decode\", s.decodeCmd, true),\n\t\tui.KeyU: ui.NewKeyAction(\"UsedBy\", s.refCmd, true),\n\t})\n}\n\nfunc (s *Secret) refCmd(evt *tcell.EventKey) *tcell.EventKey {\n\treturn scanRefs(evt, s.App(), s.GetTable(), client.SecGVR)\n}\n\nfunc (s *Secret) decodeCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := s.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\n\to, err := s.App().factory.Get(s.GVR(), path, true, labels.Everything())\n\tif err != nil {\n\t\ts.App().Flash().Err(err)\n\t\treturn nil\n\t}\n\n\tmm, err := dao.ExtractSecrets(o)\n\tif err != nil {\n\t\ts.App().Flash().Err(err)\n\t\treturn nil\n\t}\n\n\traw, err := data.WriteYAML(mm)\n\tif err != nil {\n\t\ts.App().Flash().Errf(\"Error decoding secret %s\", err)\n\t\treturn nil\n\t}\n\n\tdetails := NewDetails(s.App(), \"Secret Decoder\", path, contentYAML, true).Update(string(raw))\n\tif err := s.App().inject(details, false); err != nil {\n\t\ts.App().Flash().Err(err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/view/secret_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSecretNew(t *testing.T) {\n\ts := view.NewSecret(client.SecGVR)\n\n\trequire.NoError(t, s.Init(makeCtx(t)))\n\tassert.Equal(t, \"Secrets\", s.Name())\n\tassert.Len(t, s.Hints(), 10)\n}\n"
  },
  {
    "path": "internal/view/sts.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"errors\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n)\n\n// StatefulSet represents a statefulset viewer.\ntype StatefulSet struct {\n\tResourceViewer\n}\n\n// NewStatefulSet returns a new viewer.\nfunc NewStatefulSet(gvr *client.GVR) ResourceViewer {\n\tvar s StatefulSet\n\ts.ResourceViewer = NewPortForwardExtender(\n\t\tNewVulnerabilityExtender(\n\t\t\tNewRestartExtender(\n\t\t\t\tNewScaleExtender(\n\t\t\t\t\tNewImageExtender(\n\t\t\t\t\t\tNewOwnerExtender(\n\t\t\t\t\t\t\tNewLogsExtender(NewBrowser(gvr), s.logOptions),\n\t\t\t\t\t\t),\n\t\t\t\t\t),\n\t\t\t\t),\n\t\t\t),\n\t\t),\n\t)\n\ts.GetTable().SetEnterFn(s.showPods)\n\n\treturn &s\n}\n\nfunc (s *StatefulSet) logOptions(prev bool) (*dao.LogOptions, error) {\n\tpath := s.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn nil, errors.New(\"you must provide a selection\")\n\t}\n\tsts, err := s.getInstance(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn podLogOptions(s.App(), path, prev, &sts.ObjectMeta, &sts.Spec.Template.Spec), nil\n}\n\nfunc (s *StatefulSet) showPods(app *App, _ ui.Tabular, _ *client.GVR, path string) {\n\ti, err := s.getInstance(path)\n\tif err != nil {\n\t\tapp.Flash().Err(err)\n\t\treturn\n\t}\n\n\tshowPodsFromSelector(app, path, i.Spec.Selector)\n}\n\nfunc (s *StatefulSet) getInstance(path string) (*appsv1.StatefulSet, error) {\n\tvar sts dao.StatefulSet\n\n\treturn sts.GetInstance(s.App().factory, path)\n}\n"
  },
  {
    "path": "internal/view/sts_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestStatefulSetNew(t *testing.T) {\n\ts := view.NewStatefulSet(client.StsGVR)\n\n\trequire.NoError(t, s.Init(makeCtx(t)))\n\tassert.Equal(t, \"StatefulSets\", s.Name())\n\tassert.Len(t, s.Hints(), 14)\n}\n"
  },
  {
    "path": "internal/view/svc.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/perf\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\n// Service represents a service viewer.\ntype Service struct {\n\tResourceViewer\n\n\tbench *perf.Benchmark\n}\n\n// NewService returns a new viewer.\nfunc NewService(gvr *client.GVR) ResourceViewer {\n\ts := Service{\n\t\tResourceViewer: NewPortForwardExtender(\n\t\t\tNewOwnerExtender(\n\t\t\t\tNewLogsExtender(NewBrowser(gvr), nil),\n\t\t\t),\n\t\t),\n\t}\n\ts.AddBindKeysFn(s.bindKeys)\n\ts.GetTable().SetEnterFn(s.showPods)\n\n\treturn &s\n}\n\n// Protocol...\n\nfunc (s *Service) bindKeys(aa *ui.KeyActions) {\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyB: ui.NewKeyAction(\"Bench Run/Stop\", s.toggleBenchCmd, true),\n\t})\n}\n\nfunc (s *Service) showPods(a *App, _ ui.Tabular, _ *client.GVR, path string) {\n\tvar res dao.Service\n\tres.Init(a.factory, s.GVR())\n\n\tsvc, err := res.GetInstance(path)\n\tif err != nil {\n\t\ta.Flash().Err(err)\n\t\treturn\n\t}\n\tif svc.Spec.Type == v1.ServiceTypeExternalName {\n\t\ta.Flash().Warnf(\"No matching pods. Service %s is an external service.\", path)\n\t\treturn\n\t}\n\tif svc.Spec.Selector == nil {\n\t\ta.Flash().Warnf(\"No matching pods. Service %s does not provide any selectors\", path)\n\t\treturn\n\t}\n\n\tshowPods(a, path, labels.SelectorFromSet(svc.Spec.Selector), \"\")\n}\n\nfunc (*Service) checkSvc(svc *v1.Service) error {\n\tif svc.Spec.Type != \"NodePort\" && svc.Spec.Type != \"LoadBalancer\" {\n\t\treturn errors.New(\"you must select a reachable service\")\n\t}\n\treturn nil\n}\n\nfunc (*Service) getExternalPort(svc *v1.Service) (string, error) {\n\tif svc.Spec.Type == \"LoadBalancer\" {\n\t\treturn \"\", nil\n\t}\n\tports := render.ToPorts(svc.Spec.Ports)\n\tpp := strings.Split(ports, \" \")\n\t// Grab the first port pair for now...\n\ttokens := strings.Split(pp[0], \"►\")\n\tif len(tokens) < 2 {\n\t\treturn \"\", errors.New(\"no ports pair found\")\n\t}\n\n\treturn tokens[1], nil\n}\n\nfunc (s *Service) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif s.bench != nil {\n\t\tslog.Debug(\">>> Benchmark canceled!!\")\n\t\ts.App().Status(model.FlashErr, \"Benchmark Canceled!\")\n\t\ts.bench.Cancel()\n\t\ts.App().ClearStatus(true)\n\t\treturn nil\n\t}\n\n\tpath := s.GetTable().GetSelectedItem()\n\tif path == \"\" || s.bench != nil {\n\t\treturn evt\n\t}\n\n\tcust, err := config.NewBench(s.App().BenchFile)\n\tif err != nil {\n\t\tslog.Debug(\"No bench config file found\", slogs.FileName, s.App().BenchFile)\n\t}\n\n\tcfg, ok := cust.Benchmarks.Services[path]\n\tif !ok {\n\t\ts.App().Flash().Errf(\"No bench config found for service %s in %s\", path, s.App().BenchFile)\n\t\treturn nil\n\t}\n\tcfg.Name = path\n\tslog.Debug(\"Benchmark config\", slogs.Config, cfg)\n\n\tsvc, err := fetchService(s.App().factory, path)\n\tif err != nil {\n\t\ts.App().Flash().Err(err)\n\t\treturn nil\n\t}\n\tif e := s.checkSvc(svc); e != nil {\n\t\ts.App().Flash().Err(e)\n\t\treturn nil\n\t}\n\tport, err := s.getExternalPort(svc)\n\tif err != nil {\n\t\ts.App().Flash().Err(err)\n\t\treturn nil\n\t}\n\tif err := s.runBenchmark(port, &cfg); err != nil {\n\t\ts.App().Flash().Errf(\"Benchmark failed %v\", err)\n\t\ts.App().ClearStatus(false)\n\t\ts.bench = nil\n\t}\n\n\treturn nil\n}\n\n// BOZO!! Refactor used by forwards.\nfunc (s *Service) runBenchmark(port string, cfg *config.BenchConfig) error {\n\tif cfg.HTTP.Host == \"\" {\n\t\treturn fmt.Errorf(\"invalid benchmark host %q\", cfg.HTTP.Host)\n\t}\n\n\tvar err error\n\tbase := cfg.HTTP.Host\n\tif !strings.Contains(base, \":\") {\n\t\tbase += \":\" + port + cfg.HTTP.Path\n\t} else {\n\t\tbase += cfg.HTTP.Path\n\t}\n\tif strings.Index(base, \"http\") != 0 {\n\t\tbase = \"http://\" + base\n\t}\n\n\tif s.bench, err = perf.NewBenchmark(base, s.App().version, cfg); err != nil {\n\t\treturn err\n\t}\n\n\ts.App().Status(model.FlashWarn, \"Benchmark in progress...\")\n\tslog.Debug(\"Benchmark starting...\")\n\n\tct, err := s.App().Config.K9s.ActiveContext()\n\tif err != nil {\n\t\treturn err\n\t}\n\tname := s.App().Config.K9s.ActiveContextName()\n\n\tgo s.bench.Run(ct.ClusterName, name, s.benchDone)\n\n\treturn nil\n}\n\nfunc (s *Service) benchDone() {\n\tslog.Debug(\"Bench Completed!\")\n\ts.App().QueueUpdate(func() {\n\t\tif s.bench.Canceled() {\n\t\t\ts.App().Status(model.FlashInfo, \"Benchmark canceled\")\n\t\t} else {\n\t\t\ts.App().Status(model.FlashInfo, \"Benchmark Completed!\")\n\t\t\ts.bench.Cancel()\n\t\t}\n\t\ts.bench = nil\n\t\tgo clearStatus(s.App())\n\t})\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc clearStatus(app *App) {\n\t<-time.After(2 * time.Second)\n\tapp.QueueUpdate(func() {\n\t\tapp.ClearStatus(true)\n\t})\n}\n\nfunc fetchService(f dao.Factory, path string) (*v1.Service, error) {\n\to, err := f.Get(client.SvcGVR, path, true, labels.Everything())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar svc v1.Service\n\terr = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &svc)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &svc, nil\n}\n"
  },
  {
    "path": "internal/view/svc_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/view\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\nfunc init() {\n\tdao.MetaAccess.RegisterMeta(client.DirGVR.String(), &metav1.APIResource{\n\t\tName:         \"dirs\",\n\t\tSingularName: \"dir\",\n\t\tKind:         \"Directory\",\n\t\tCategories:   []string{\"k9s\"},\n\t})\n\tdao.MetaAccess.RegisterMeta(client.PodGVR.String(), &metav1.APIResource{\n\t\tName:         \"pods\",\n\t\tSingularName: \"pod\",\n\t\tNamespaced:   true,\n\t\tKind:         \"Pods\",\n\t\tVerbs:        []string{\"get\", \"list\", \"watch\", \"delete\"},\n\t\tCategories:   []string{\"k9s\"},\n\t})\n\tdao.MetaAccess.RegisterMeta(client.NsGVR.String(), &metav1.APIResource{\n\t\tName:         \"namespaces\",\n\t\tSingularName: \"namespace\",\n\t\tNamespaced:   true,\n\t\tKind:         \"Namespaces\",\n\t\tVerbs:        []string{\"get\", \"list\", \"watch\", \"delete\"},\n\t\tCategories:   []string{\"k9s\"},\n\t})\n\tdao.MetaAccess.RegisterMeta(client.SvcGVR.String(), &metav1.APIResource{\n\t\tName:         \"services\",\n\t\tSingularName: \"service\",\n\t\tNamespaced:   true,\n\t\tKind:         \"Services\",\n\t\tVerbs:        []string{\"get\", \"list\", \"watch\", \"delete\"},\n\t\tCategories:   []string{\"k9s\"},\n\t})\n\tdao.MetaAccess.RegisterMeta(client.SecGVR.String(), &metav1.APIResource{\n\t\tName:         \"secrets\",\n\t\tSingularName: \"secret\",\n\t\tNamespaced:   true,\n\t\tKind:         \"Secrets\",\n\t\tVerbs:        []string{\"get\", \"list\", \"watch\", \"delete\"},\n\t\tCategories:   []string{\"k9s\"},\n\t})\n\tdao.MetaAccess.RegisterMeta(client.PcGVR.String(), &metav1.APIResource{\n\t\tName:         \"priorityclasses\",\n\t\tSingularName: \"priorityclass\",\n\t\tNamespaced:   false,\n\t\tKind:         \"PriorityClass\",\n\t\tVerbs:        []string{\"get\", \"list\", \"watch\", \"delete\"},\n\t\tCategories:   []string{\"k9s\"},\n\t})\n\tdao.MetaAccess.RegisterMeta(client.CmGVR.String(), &metav1.APIResource{\n\t\tName:         \"configmaps\",\n\t\tSingularName: \"configmap\",\n\t\tNamespaced:   true,\n\t\tKind:         \"ConfigMaps\",\n\t\tVerbs:        []string{\"get\", \"list\", \"watch\", \"delete\"},\n\t\tCategories:   []string{\"k9s\"},\n\t})\n\n\tdao.MetaAccess.RegisterMeta(client.RefGVR.String(), &metav1.APIResource{\n\t\tName:         \"references\",\n\t\tSingularName: \"reference\",\n\t\tNamespaced:   true,\n\t\tKind:         \"References\",\n\t\tVerbs:        []string{\"get\", \"list\", \"watch\", \"delete\"},\n\t\tCategories:   []string{\"k9s\"},\n\t})\n\tdao.MetaAccess.RegisterMeta(client.AliGVR.String(), &metav1.APIResource{\n\t\tName:         \"aliases\",\n\t\tSingularName: \"alias\",\n\t\tNamespaced:   true,\n\t\tKind:         \"Aliases\",\n\t\tVerbs:        []string{\"get\", \"list\", \"watch\", \"delete\"},\n\t\tCategories:   []string{\"k9s\"},\n\t})\n\tdao.MetaAccess.RegisterMeta(client.CoGVR.String(), &metav1.APIResource{\n\t\tName:         \"containers\",\n\t\tSingularName: \"container\",\n\t\tNamespaced:   true,\n\t\tKind:         \"Containers\",\n\t\tVerbs:        []string{\"get\", \"list\", \"watch\", \"delete\"},\n\t\tCategories:   []string{\"k9s\"},\n\t})\n\tdao.MetaAccess.RegisterMeta(client.CtGVR.String(), &metav1.APIResource{\n\t\tName:         \"contexts\",\n\t\tSingularName: \"context\",\n\t\tNamespaced:   true,\n\t\tKind:         \"Contexts\",\n\t\tVerbs:        []string{\"get\", \"list\", \"watch\", \"delete\"},\n\t\tCategories:   []string{\"k9s\"},\n\t})\n\tdao.MetaAccess.RegisterMeta(\"subjects\", &metav1.APIResource{\n\t\tName:         \"subjects\",\n\t\tSingularName: \"subject\",\n\t\tNamespaced:   true,\n\t\tKind:         \"Subjects\",\n\t\tVerbs:        []string{\"get\", \"list\", \"watch\", \"delete\"},\n\t\tCategories:   []string{\"k9s\"},\n\t})\n\tdao.MetaAccess.RegisterMeta(client.RbacGVR.String(), &metav1.APIResource{\n\t\tName:         \"rbacs\",\n\t\tSingularName: \"rbac\",\n\t\tNamespaced:   true,\n\t\tKind:         \"Rbac\",\n\t\tVerbs:        []string{\"get\", \"list\", \"watch\", \"delete\"},\n\t\tCategories:   []string{\"k9s\"},\n\t})\n\tdao.MetaAccess.RegisterMeta(client.PfGVR.String(), &metav1.APIResource{\n\t\tName:         \"portforwards\",\n\t\tSingularName: \"portforward\",\n\t\tNamespaced:   true,\n\t\tKind:         \"PortForwards\",\n\t\tVerbs:        []string{\"get\", \"list\", \"watch\", \"delete\"},\n\t\tCategories:   []string{\"k9s\"},\n\t})\n\n\tdao.MetaAccess.RegisterMeta(client.SdGVR.String(), &metav1.APIResource{\n\t\tName:         \"screendumps\",\n\t\tSingularName: \"screendump\",\n\t\tNamespaced:   true,\n\t\tKind:         \"ScreenDumps\",\n\t\tVerbs:        []string{\"get\", \"list\", \"watch\", \"delete\"},\n\t\tCategories:   []string{\"k9s\"},\n\t})\n\tdao.MetaAccess.RegisterMeta(client.StsGVR.String(), &metav1.APIResource{\n\t\tName:         \"statefulsets\",\n\t\tSingularName: \"statefulset\",\n\t\tNamespaced:   true,\n\t\tKind:         \"StatefulSets\",\n\t\tVerbs:        []string{\"get\", \"list\", \"watch\", \"delete\"},\n\t\tCategories:   []string{\"k9s\"},\n\t})\n\tdao.MetaAccess.RegisterMeta(client.DsGVR.String(), &metav1.APIResource{\n\t\tName:         \"daemonsets\",\n\t\tSingularName: \"daemonset\",\n\t\tNamespaced:   true,\n\t\tKind:         \"DaemonSets\",\n\t\tVerbs:        []string{\"get\", \"list\", \"watch\", \"delete\"},\n\t\tCategories:   []string{\"k9s\"},\n\t})\n\tdao.MetaAccess.RegisterMeta(client.DpGVR.String(), &metav1.APIResource{\n\t\tName:         \"deployments\",\n\t\tSingularName: \"deployment\",\n\t\tNamespaced:   true,\n\t\tKind:         \"Deployments\",\n\t\tVerbs:        []string{\"get\", \"list\", \"watch\", \"delete\"},\n\t\tCategories:   []string{\"k9s\"},\n\t})\n\tdao.MetaAccess.RegisterMeta(client.PvcGVR.String(), &metav1.APIResource{\n\t\tName:         \"persistentvolumeclaims\",\n\t\tSingularName: \"persistentvolumeclaim\",\n\t\tNamespaced:   true,\n\t\tKind:         \"PersistentVolumeClaims\",\n\t\tVerbs:        []string{\"get\", \"list\", \"watch\", \"delete\"},\n\t\tCategories:   []string{\"k9s\"},\n\t})\n}\n\nfunc TestServiceNew(t *testing.T) {\n\ts := view.NewService(client.SvcGVR)\n\n\trequire.NoError(t, s.Init(makeCtx(t)))\n\tassert.Equal(t, \"Services\", s.Name())\n\tassert.Len(t, s.Hints(), 13)\n}\n"
  },
  {
    "path": "internal/view/table.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\n// Table represents a table viewer.\ntype Table struct {\n\t*ui.Table\n\n\tapp        *App\n\tenterFn    EnterFunc\n\tenvFn      EnvFunc\n\tbindKeysFn []BindKeysFunc\n\tcommand    *cmd.Interpreter\n}\n\n// NewTable returns a new viewer.\nfunc NewTable(gvr *client.GVR) *Table {\n\tt := Table{\n\t\tTable: ui.NewTable(gvr),\n\t}\n\tt.envFn = t.defaultEnv\n\n\treturn &t\n}\n\n// Init initializes the component.\nfunc (t *Table) Init(ctx context.Context) (err error) {\n\tif t.app, err = extractApp(ctx); err != nil {\n\t\treturn err\n\t}\n\tif t.app.Conn() != nil {\n\t\tctx = context.WithValue(ctx, internal.KeyHasMetrics, t.app.Conn().HasMetrics())\n\t}\n\tctx = context.WithValue(ctx, internal.KeyStyles, t.app.Styles)\n\tctx = context.WithValue(ctx, internal.KeyViewConfig, t.app.CustomView())\n\tt.Table.Init(ctx)\n\tif !t.app.Config.K9s.UI.Reactive {\n\t\tif err := t.app.RefreshCustomViews(); err != nil {\n\t\t\tslog.Warn(\"CustomViews load failed\", slogs.Error, err)\n\t\t\tt.app.Logo().Warn(\"Views load failed!\")\n\t\t}\n\t}\n\tt.SetInputCapture(t.keyboard)\n\tt.bindKeys()\n\tt.GetModel().SetRefreshRate(t.app.Config.K9s.RefreshDuration())\n\tt.CmdBuff().AddListener(t)\n\n\treturn nil\n}\n\n// SetCommand sets the current command.\nfunc (t *Table) SetCommand(i *cmd.Interpreter) {\n\tt.command = i\n}\n\n// HeaderIndex returns index of a given column or false if not found.\nfunc (t *Table) HeaderIndex(colName string) (int, bool) {\n\tfor i := range t.GetColumnCount() {\n\t\th := t.GetCell(0, i)\n\t\tif h == nil {\n\t\t\tcontinue\n\t\t}\n\t\ts := h.Text\n\t\tif idx := strings.Index(s, \"[\"); idx > 0 {\n\t\t\ts = s[:idx]\n\t\t}\n\t\tif s == colName {\n\t\t\treturn i, true\n\t\t}\n\t}\n\treturn 0, false\n}\n\n// SendKey sends a keyboard event (testing only!).\nfunc (t *Table) SendKey(evt *tcell.EventKey) {\n\tt.app.Prompt().SendKey(evt)\n}\n\nfunc (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey {\n\tkey := evt.Key()\n\n\t// Handle Shift+Left/Right for column selection\n\tif evt.Modifiers()&tcell.ModShift != 0 {\n\t\tif key == tcell.KeyLeft {\n\t\t\tt.Table.SelectPrevColumn()\n\t\t\treturn nil\n\t\t}\n\t\tif key == tcell.KeyRight {\n\t\t\tt.Table.SelectNextColumn()\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tif key == tcell.KeyUp || key == tcell.KeyDown {\n\t\treturn evt\n\t}\n\n\tif a, ok := t.Actions().Get(ui.AsKey(evt)); ok && !t.app.Content.IsTopDialog() {\n\t\treturn a.Action(evt)\n\t}\n\n\treturn evt\n}\n\n// Name returns the table name.\nfunc (t *Table) Name() string { return t.GVR().R() }\n\n// AddBindKeysFn adds additional key bindings.\nfunc (t *Table) AddBindKeysFn(f BindKeysFunc) {\n\tt.bindKeysFn = append(t.bindKeysFn, f)\n}\n\n// SetEnvFn sets a function to pull viewer env vars for plugins.\nfunc (t *Table) SetEnvFn(f EnvFunc) { t.envFn = f }\n\n// EnvFn returns an plugin env function if available.\nfunc (t *Table) EnvFn() EnvFunc {\n\treturn t.envFn\n}\n\nfunc (t *Table) defaultEnv() Env {\n\tpath := t.GetSelectedItem()\n\trow := t.GetSelectedRow(path)\n\tenv := defaultEnv(t.app.Conn().Config(), path, t.GetModel().Peek().Header(), row)\n\tenv[\"FILTER\"] = t.CmdBuff().GetText()\n\tif env[\"FILTER\"] == \"\" {\n\t\tenv[\"NAMESPACE\"], env[\"FILTER\"] = client.Namespaced(path)\n\t}\n\tenv[\"RESOURCE_GROUP\"] = t.GVR().G()\n\tenv[\"RESOURCE_VERSION\"] = t.GVR().V()\n\tenv[\"RESOURCE_NAME\"] = t.GVR().R()\n\n\treturn env\n}\n\n// App returns the current app handle.\nfunc (t *Table) App() *App {\n\treturn t.app\n}\n\n// Start runs the component.\nfunc (t *Table) Start() {\n\tt.Stop()\n\tt.CmdBuff().AddListener(t)\n\tt.Styles().AddListener(t.Table)\n\tcmds := []string{t.Table.GVR().String()}\n\tif t.command != nil {\n\t\tif t.command.GetLine() != t.Table.GVR().String() {\n\t\t\tcmds = append(cmds, t.command.GetLine())\n\t\t}\n\t\tfor _, a := range t.command.Aliases() {\n\t\t\tcmds = append(cmds, a)\n\t\t}\n\t}\n\tt.App().CustomView().AddListeners(t.Table, cmds...)\n}\n\n// Stop terminates the component.\nfunc (t *Table) Stop() {\n\tt.CmdBuff().RemoveListener(t)\n\tt.Styles().RemoveListener(t.Table)\n\tt.App().CustomView().RemoveListener(t.Table)\n}\n\n// SetEnterFn specifies the default enter behavior.\nfunc (t *Table) SetEnterFn(f EnterFunc) {\n\tt.enterFn = f\n}\n\n// SetExtraActionsFn specifies custom keyboard behavior.\nfunc (*Table) SetExtraActionsFn(BoostActionsFunc) {}\n\n// BufferCompleted indicates input was accepted.\nfunc (t *Table) BufferCompleted(text, _ string) {\n\tt.app.QueueUpdateDraw(func() {\n\t\tt.Filter(text)\n\t})\n}\n\n// BufferChanged indicates the buffer was changed.\nfunc (*Table) BufferChanged(_, _ string) {}\n\n// BufferActive indicates the buff activity changed.\nfunc (t *Table) BufferActive(state bool, k model.BufferKind) {\n\tt.app.BufferActive(state, k)\n\tif !state {\n\t\tt.app.SetFocus(t)\n\t}\n}\n\nfunc (t *Table) saveCmd(*tcell.EventKey) *tcell.EventKey {\n\tif path, err := saveTable(t.app.Config.K9s.ContextScreenDumpDir(), t.GVR().R(), t.Path, t.GetFilteredData()); err != nil {\n\t\tt.app.Flash().Err(err)\n\t} else {\n\t\tt.app.Flash().Infof(\"File saved successfully: %q\", render.Truncate(filepath.Base(path), 50))\n\t}\n\n\treturn nil\n}\n\nfunc (t *Table) bindKeys() {\n\tt.Actions().Bulk(ui.KeyMap{\n\t\tui.KeyHelp:             ui.NewKeyAction(\"Help\", t.App().helpCmd, true),\n\t\tui.KeySpace:            ui.NewSharedKeyAction(\"Mark\", t.markCmd, false),\n\t\ttcell.KeyCtrlSpace:     ui.NewSharedKeyAction(\"Mark Range\", t.markSpanCmd, false),\n\t\ttcell.KeyCtrlBackslash: ui.NewSharedKeyAction(\"Marks Clear\", t.clearMarksCmd, false),\n\t\ttcell.KeyCtrlS:         ui.NewSharedKeyAction(\"Save\", t.saveCmd, false),\n\t\tui.KeySlash:            ui.NewSharedKeyAction(\"Filter Mode\", t.activateCmd, false),\n\t\ttcell.KeyCtrlZ:         ui.NewKeyAction(\"Toggle Faults\", t.toggleFaultCmd, false),\n\t\ttcell.KeyCtrlW:         ui.NewKeyAction(\"Toggle Wide\", t.toggleWideCmd, false),\n\t\tui.KeyShiftN:           ui.NewKeyAction(\"Sort Name\", t.SortColCmd(nameCol, true), false),\n\t\tui.KeyShiftA:           ui.NewKeyAction(\"Sort Age\", t.SortColCmd(ageCol, true), false),\n\t\tui.KeyShiftS:           ui.NewKeyAction(\"Sort Status\", t.SortColCmd(statusCol, true), false),\n\t\tui.KeyShiftO:           ui.NewKeyAction(\"Sort Selected Column\", t.sortSelectedColumnCmd, false),\n\t})\n}\n\nfunc (t *Table) toggleFaultCmd(*tcell.EventKey) *tcell.EventKey {\n\tt.ToggleToast()\n\treturn nil\n}\n\nfunc (t *Table) toggleWideCmd(*tcell.EventKey) *tcell.EventKey {\n\tt.ToggleWide()\n\treturn nil\n}\n\nfunc (t *Table) sortSelectedColumnCmd(*tcell.EventKey) *tcell.EventKey {\n\tt.Table.SortSelectedColumn()\n\treturn nil\n}\n\nfunc (t *Table) cpCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpaths := t.GetSelectedItems()\n\tif len(paths) == 0 {\n\t\treturn evt\n\t}\n\n\tnames := make([]string, 0, len(paths))\n\tfor _, path := range paths {\n\t\t_, n := client.Namespaced(path)\n\t\tnames = append(names, n)\n\t}\n\n\ttext := strings.Join(names, \"\\n\")\n\tif err := clipboardWrite(text); err != nil {\n\t\tt.app.Flash().Err(err)\n\t\treturn nil\n\t}\n\n\tif len(names) > 1 {\n\t\tt.app.Flash().Infof(\"%d resource names copied to clipboard...\", len(names))\n\t} else {\n\t\tt.app.Flash().Info(\"Resource name copied to clipboard...\")\n\t}\n\n\treturn nil\n}\n\nfunc (t *Table) cpNsCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpaths := t.GetSelectedItems()\n\tif len(paths) == 0 {\n\t\treturn evt\n\t}\n\n\tnamespaces := make([]string, 0, len(paths))\n\tfor _, path := range paths {\n\t\tns, _ := client.Namespaced(path)\n\t\tnamespaces = append(namespaces, ns)\n\t}\n\n\ttext := strings.Join(namespaces, \"\\n\")\n\tif err := clipboardWrite(text); err != nil {\n\t\tt.app.Flash().Err(err)\n\t\treturn nil\n\t}\n\n\tif len(namespaces) > 1 {\n\t\tt.app.Flash().Infof(\"%d resource namespaces copied to clipboard...\", len(namespaces))\n\t} else {\n\t\tt.app.Flash().Info(\"Resource namespace copied to clipboard...\")\n\t}\n\n\treturn nil\n}\n\nfunc (t *Table) markCmd(*tcell.EventKey) *tcell.EventKey {\n\tt.ToggleMark()\n\tt.Refresh()\n\n\treturn nil\n}\n\nfunc (t *Table) markSpanCmd(*tcell.EventKey) *tcell.EventKey {\n\tt.SpanMark()\n\tt.Refresh()\n\n\treturn nil\n}\n\nfunc (t *Table) clearMarksCmd(*tcell.EventKey) *tcell.EventKey {\n\tt.ClearMarks()\n\tt.Refresh()\n\n\treturn nil\n}\n\nfunc (t *Table) activateCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif t.app.InCmdMode() {\n\t\treturn evt\n\t}\n\tt.App().ResetPrompt(t.CmdBuff())\n\n\treturn evt\n}\n"
  },
  {
    "path": "internal/view/table_helper.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"encoding/csv\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n)\n\nfunc computeFilename(dumpPath, ns, title, path string) (string, error) {\n\tnow := time.Now().UnixNano()\n\n\tdir := dumpPath\n\tif err := ensureDir(dir); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tname := title + \"-\" + data.SanitizeFileName(path)\n\tif path == \"\" {\n\t\tname = title\n\t}\n\n\tvar fName string\n\tif ns == client.ClusterScope {\n\t\tfName = fmt.Sprintf(ui.NoNSFmat, name, now)\n\t} else {\n\t\tfName = fmt.Sprintf(ui.FullFmat, name, ns, now)\n\t}\n\n\treturn strings.ToLower(filepath.Join(dir, fName)), nil\n}\n\nfunc saveTable(dir, title, path string, mdata *model1.TableData) (string, error) {\n\tns := mdata.GetNamespace()\n\tif client.IsClusterWide(ns) {\n\t\tns = client.NamespaceAll\n\t}\n\n\tfPath, err := computeFilename(dir, ns, title, path)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tslog.Debug(\"Saving table to disk\", slogs.FileName, fPath)\n\n\tmod := os.O_CREATE | os.O_WRONLY\n\tout, err := os.OpenFile(fPath, mod, 0600)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer func() {\n\t\tif err := out.Close(); err != nil {\n\t\t\tslog.Error(\"Closing file failed\",\n\t\t\t\tslogs.Path, fPath,\n\t\t\t\tslogs.Error, err,\n\t\t\t)\n\t\t}\n\t}()\n\n\tw := csv.NewWriter(out)\n\t_ = w.Write(mdata.ColumnNames(true))\n\n\tmdata.RowsRange(func(_ int, re model1.RowEvent) bool {\n\t\t_ = w.Write(re.Row.Fields)\n\t\treturn true\n\t})\n\tw.Flush()\n\tif err := w.Error(); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn fPath, nil\n}\n"
  },
  {
    "path": "internal/view/table_int_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"io/fs\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/config/mock\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/model1\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tview\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nfunc TestTableSave(t *testing.T) {\n\tv := NewTable(client.NewGVR(\"test\"))\n\trequire.NoError(t, v.Init(makeContext(t)))\n\tv.SetTitle(\"k9s-test\")\n\n\trequire.NoError(t, ensureDumpDir(\"/tmp/test-dumps\"))\n\tdir := v.app.Config.K9s.ContextScreenDumpDir()\n\tc1, _ := os.ReadDir(dir)\n\tv.saveCmd(nil)\n\n\tc2, _ := os.ReadDir(dir)\n\tassert.Len(t, c2, len(c1)+1)\n}\n\nfunc TestTableNew(t *testing.T) {\n\tv := NewTable(client.NewGVR(\"test\"))\n\trequire.NoError(t, v.Init(makeContext(t)))\n\n\tdata := model1.NewTableDataWithRows(\n\t\tclient.NewGVR(\"test\"),\n\t\tmodel1.Header{\n\t\t\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\t\t\tmodel1.HeaderColumn{Name: \"NAME\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\t\t\tmodel1.HeaderColumn{Name: \"FRED\"},\n\t\t\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true, Decorator: render.AgeDecorator}},\n\t\t},\n\t\tmodel1.NewRowEventsWithEvts(\n\t\t\tmodel1.RowEvent{\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"ns1\", \"a\", \"10\", \"3m\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodel1.RowEvent{\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"ns1\", \"b\", \"15\", \"1m\"},\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t)\n\tcdata := v.Update(data, false)\n\tv.UpdateUI(cdata, data)\n\n\tassert.Equal(t, 3, v.GetRowCount())\n}\n\nfunc TestTableViewFilter(t *testing.T) {\n\tv := NewTable(client.NewGVR(\"test\"))\n\trequire.NoError(t, v.Init(makeContext(t)))\n\tv.SetModel(&mockTableModel{})\n\tv.Refresh()\n\n\tv.CmdBuff().SetActive(true)\n\tv.CmdBuff().SetText(\"blee\", \"\", true)\n\n\tassert.Equal(t, 5, v.GetRowCount())\n}\n\nfunc TestTableViewSort(t *testing.T) {\n\tv := NewTable(client.NewGVR(\"test\"))\n\trequire.NoError(t, v.Init(makeContext(t)))\n\tv.SetModel(new(mockTableModel))\n\n\tuu := map[string]struct {\n\t\tsortCol  string\n\t\tsorted   []string\n\t\treversed []string\n\t}{\n\t\t\"by_name\": {\n\t\t\tsortCol:  \"NAME\",\n\t\t\tsorted:   []string{\"r0\", \"r1\", \"r2\", \"r3\"},\n\t\t\treversed: []string{\"r3\", \"r2\", \"r1\", \"r0\"},\n\t\t},\n\t\t\"by_age\": {\n\t\t\tsortCol:  \"AGE\",\n\t\t\tsorted:   []string{\"r0\", \"r1\", \"r2\", \"r3\"},\n\t\t\treversed: []string{\"r3\", \"r2\", \"r1\", \"r0\"},\n\t\t},\n\t\t\"by_fred\": {\n\t\t\tsortCol:  \"FRED\",\n\t\t\tsorted:   []string{\"r3\", \"r2\", \"r0\", \"r1\"},\n\t\t\treversed: []string{\"r1\", \"r0\", \"r2\", \"r3\"},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tv.SortColCmd(u.sortCol, true)(nil)\n\t\tassert.Len(t, u.sorted, v.GetRowCount()-1)\n\t\tfor i, s := range u.sorted {\n\t\t\tassert.Equal(t, s, v.GetCell(i+1, 0).Text)\n\t\t}\n\t\tv.SortInvertCmd(nil)\n\t\tassert.Len(t, u.reversed, v.GetRowCount()-1)\n\t\tfor i, s := range u.reversed {\n\t\t\tassert.Equal(t, s, v.GetCell(i+1, 0).Text)\n\t\t}\n\t}\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\ntype mockTableModel struct{}\n\nvar _ ui.Tabular = (*mockTableModel)(nil)\n\nfunc (*mockTableModel) SetViewSetting(context.Context, *config.ViewSetting) {}\nfunc (*mockTableModel) SetInstance(string)                                  {}\nfunc (*mockTableModel) SetLabelSelector(labels.Selector)                    {}\nfunc (*mockTableModel) GetLabelSelector() labels.Selector                   { return nil }\nfunc (*mockTableModel) Empty() bool                                         { return false }\nfunc (*mockTableModel) RowCount() int                                       { return 1 }\nfunc (*mockTableModel) HasMetrics() bool                                    { return true }\nfunc (*mockTableModel) Peek() *model1.TableData                             { return makeTableData() }\nfunc (*mockTableModel) Refresh(context.Context) error                       { return nil }\nfunc (*mockTableModel) ClusterWide() bool                                   { return false }\nfunc (*mockTableModel) GetNamespace() string                                { return \"blee\" }\nfunc (*mockTableModel) SetNamespace(string)                                 {}\nfunc (*mockTableModel) ToggleToast()                                        {}\nfunc (*mockTableModel) AddListener(model.TableListener)                     {}\nfunc (*mockTableModel) RemoveListener(model.TableListener)                  {}\nfunc (*mockTableModel) Watch(context.Context) error                         { return nil }\nfunc (*mockTableModel) Get(context.Context, string) (runtime.Object, error) {\n\treturn nil, nil\n}\nfunc (*mockTableModel) Delete(context.Context, string, *metav1.DeletionPropagation, dao.Grace) error {\n\treturn nil\n}\nfunc (*mockTableModel) Describe(context.Context, string) (string, error) {\n\treturn \"\", nil\n}\nfunc (*mockTableModel) ToYAML(context.Context, string) (string, error) {\n\treturn \"\", nil\n}\nfunc (*mockTableModel) InNamespace(string) bool      { return true }\nfunc (*mockTableModel) SetRefreshRate(time.Duration) {}\n\nfunc makeTableData() *model1.TableData {\n\treturn model1.NewTableDataWithRows(\n\t\tclient.NewGVR(\"test\"),\n\t\tmodel1.Header{\n\t\t\tmodel1.HeaderColumn{Name: \"NAMESPACE\"},\n\t\t\tmodel1.HeaderColumn{Name: \"NAME\", Attrs: model1.Attrs{Align: tview.AlignRight}},\n\t\t\tmodel1.HeaderColumn{Name: \"FRED\"},\n\t\t\tmodel1.HeaderColumn{Name: \"AGE\", Attrs: model1.Attrs{Time: true}},\n\t\t},\n\t\tmodel1.NewRowEventsWithEvts(\n\t\t\tmodel1.RowEvent{\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"ns1\", \"r3\", \"10\", \"3y125d\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodel1.RowEvent{\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"ns1\", \"r2\", \"15\", \"2y12d\"},\n\t\t\t\t},\n\t\t\t\tDeltas: model1.DeltaRow{\"\", \"\", \"20\", \"\"},\n\t\t\t},\n\t\t\tmodel1.RowEvent{\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"ns1\", \"r1\", \"20\", \"19h\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmodel1.RowEvent{\n\t\t\t\tRow: model1.Row{\n\t\t\t\t\tFields: model1.Fields{\"ns1\", \"r0\", \"15\", \"10s\"},\n\t\t\t\t},\n\t\t\t},\n\t\t),\n\t)\n}\n\nfunc makeContext(t *testing.T) context.Context {\n\ta := NewApp(mock.NewMockConfig(t))\n\tctx := context.WithValue(context.Background(), internal.KeyApp, a)\n\treturn context.WithValue(ctx, internal.KeyStyles, a.Styles)\n}\n\nfunc ensureDumpDir(n string) error {\n\tconfig.AppDumpsDir = n\n\tif _, err := os.Stat(n); errors.Is(err, fs.ErrNotExist) {\n\t\treturn os.Mkdir(n, 0700)\n\t}\n\tif err := os.RemoveAll(n); err != nil {\n\t\treturn err\n\t}\n\treturn os.Mkdir(n, 0700)\n}\n"
  },
  {
    "path": "internal/view/testdata/fred/kmanifests/cm.yaml",
    "content": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: the-map\ndata:\n  altGreeting: \"Good Morning!\"\n  enableRisky: \"false\"\n"
  },
  {
    "path": "internal/view/testdata/fred/kmanifests/kustomization.yaml",
    "content": "commonLabels:\n  app: fred\n\nresources:\n  - cm.yaml\n"
  },
  {
    "path": "internal/view/testdata/k1manifests/cm.yaml",
    "content": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: the-map\ndata:\n  altGreeting: \"Good Morning!\"\n  enableRisky: \"false\"\n"
  },
  {
    "path": "internal/view/testdata/k1manifests/kustomization.yml",
    "content": "commonLabels:\n  app: fred\n\nresources:\n  - cm.yaml\n"
  },
  {
    "path": "internal/view/testdata/k2manifests/Kustomization",
    "content": "commonLabels:\n  app: fred\n\nresources:\n  - cm.yaml\n"
  },
  {
    "path": "internal/view/testdata/k2manifests/cm.yaml",
    "content": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: the-map\ndata:\n  altGreeting: \"Good Morning!\"\n  enableRisky: \"false\"\n"
  },
  {
    "path": "internal/view/testdata/kmanifests/cm.yaml",
    "content": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: the-map\ndata:\n  altGreeting: \"Good Morning!\"\n  enableRisky: \"false\"\n"
  },
  {
    "path": "internal/view/testdata/kmanifests/kustomization.yaml",
    "content": "commonLabels:\n  app: fred\n\nresources:\n  - cm.yaml\n"
  },
  {
    "path": "internal/view/testdata/manifests/cm.yaml",
    "content": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: the-map\ndata:\n  altGreeting: \"Good Morning!\"\n  enableRisky: \"false\"\n"
  },
  {
    "path": "internal/view/testdata/manifests/kustomization.yaml",
    "content": "commonLabels:\n  app: fred\n\nresources:\n  - cm.yaml\n"
  },
  {
    "path": "internal/view/types.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n)\n\nconst (\n\tageCol      = \"AGE\"\n\tnameCol     = \"NAME\"\n\tstatusCol   = \"STATUS\"\n\tcpuCol      = \"CPU\"\n\tmemCol      = \"MEM\"\n\tuptodateCol = \"UP-TO-DATE\"\n\treadyCol    = \"READY\"\n\tavailCol    = \"AVAILABLE\"\n)\n\ntype (\n\t// EnvFunc represent the current view exposed environment.\n\tEnvFunc func() Env\n\n\t// BoostActionsFunc extends viewer keyboard actions.\n\tBoostActionsFunc func(ui.KeyActions)\n\n\t// EnterFunc represents an enter key action.\n\tEnterFunc func(app *App, model ui.Tabular, gvr *client.GVR, path string)\n\n\t// LogOptionsFunc returns the active log options.\n\tLogOptionsFunc func(bool) (*dao.LogOptions, error)\n\n\t// ContextFunc enhances a given context.\n\tContextFunc func(context.Context) context.Context\n\n\t// BindKeysFunc adds new menu actions.\n\tBindKeysFunc func(*ui.KeyActions)\n)\n\n// ActionExtender enhances a given viewer by adding new menu actions.\ntype ActionExtender interface {\n\t// BindKeys injects new menu actions.\n\tBindKeys(ResourceViewer)\n}\n\n// Hinter represents a view that can produce menu hints.\ntype Hinter interface {\n\t// Hints returns a collection of hints.\n\tHints() model.MenuHints\n}\n\n// Viewer represents a component viewer.\ntype Viewer interface {\n\tmodel.Component\n\n\t// Actions returns active menu bindings.\n\tActions() *ui.KeyActions\n\n\t// App returns an app handle.\n\tApp() *App\n\n\t// Refresh updates the viewer\n\tRefresh()\n}\n\n// TableViewer represents a tabular viewer.\ntype TableViewer interface {\n\tViewer\n\n\t// GetTable returns a table component.\n\tGetTable() *Table\n}\n\n// ResourceViewer represents a generic resource viewer.\ntype ResourceViewer interface {\n\tTableViewer\n\n\t// SetEnvFn sets a function to pull viewer env vars for plugins.\n\tSetEnvFn(EnvFunc)\n\n\t// GVR returns a resource descriptor.\n\tGVR() *client.GVR\n\n\t// SetContextFn provision a custom context.\n\tSetContextFn(ContextFunc)\n\n\t// AddBindKeysFn provision additional key bindings.\n\tAddBindKeysFn(BindKeysFunc)\n\n\t// SetInstance sets a parent FQN\n\tSetInstance(string)\n\n\t// SetCommand sets the current command.\n\tSetCommand(*cmd.Interpreter)\n}\n\n// LogViewer represents a log viewer.\ntype LogViewer interface {\n\tResourceViewer\n\n\tShowLogs(prev bool)\n}\n\n// RestartableViewer represents a viewer with restartable resources.\ntype RestartableViewer interface {\n\tLogViewer\n}\n\n// ScalableViewer represents a viewer with scalable resources.\ntype ScalableViewer interface {\n\tLogViewer\n}\n\n// SubjectViewer represents a policy viewer.\ntype SubjectViewer interface {\n\tResourceViewer\n\n\t// SetSubject sets the active subject.\n\tSetSubject(s string)\n}\n\n// ViewerFunc returns a viewer matching a given gvr.\ntype ViewerFunc func(*client.GVR) ResourceViewer\n\n// MetaViewer represents a registered meta viewer.\ntype MetaViewer struct {\n\tviewerFn ViewerFunc\n\tenterFn  EnterFunc\n}\n\n// MetaViewers represents a collection of meta viewers.\ntype MetaViewers map[*client.GVR]MetaViewer\n"
  },
  {
    "path": "internal/view/user.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\n// User presents a user viewer.\ntype User struct {\n\tResourceViewer\n}\n\n// NewUser returns a new subject viewer.\nfunc NewUser(gvr *client.GVR) ResourceViewer {\n\tu := User{ResourceViewer: NewBrowser(gvr)}\n\tu.AddBindKeysFn(u.bindKeys)\n\tu.SetContextFn(u.subjectCtx)\n\n\treturn &u\n}\n\nfunc (u *User) bindKeys(aa *ui.KeyActions) {\n\taa.Delete(ui.KeyShiftA, ui.KeyShiftP, tcell.KeyCtrlSpace, ui.KeySpace, tcell.KeyCtrlD, ui.KeyE)\n\taa.Bulk(ui.KeyMap{\n\t\ttcell.KeyEnter: ui.NewKeyAction(\"Rules\", u.policyCmd, true),\n\t})\n}\n\nfunc (*User) subjectCtx(ctx context.Context) context.Context {\n\treturn context.WithValue(ctx, internal.KeySubjectKind, \"User\")\n}\n\nfunc (u *User) policyCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := u.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\tif err := u.App().inject(NewPolicy(u.App(), \"User\", path), false); err != nil {\n\t\tu.App().Flash().Err(err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/view/value_extender.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"log/slog\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\n// ValueExtender adds values actions to a given viewer.\ntype ValueExtender struct {\n\tResourceViewer\n}\n\n// NewValueExtender returns a new extender.\nfunc NewValueExtender(r ResourceViewer) ResourceViewer {\n\tp := ValueExtender{ResourceViewer: r}\n\tp.AddBindKeysFn(p.bindKeys)\n\tp.GetTable().SetEnterFn(func(*App, ui.Tabular, *client.GVR, string) {\n\t\tp.valuesCmd(nil)\n\t})\n\n\treturn &p\n}\n\nfunc (v *ValueExtender) bindKeys(aa *ui.KeyActions) {\n\taa.Add(ui.KeyV, ui.NewKeyAction(\"Values\", v.valuesCmd, true))\n}\n\nfunc (v *ValueExtender) valuesCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := v.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\n\tshowValues(v.defaultCtx(), v.App(), path, v.GVR())\n\treturn nil\n}\n\nfunc (v *ValueExtender) defaultCtx() context.Context {\n\treturn context.WithValue(context.Background(), internal.KeyFactory, v.App().factory)\n}\n\nfunc showValues(ctx context.Context, app *App, path string, gvr *client.GVR) {\n\tvm := model.NewValues(gvr, path)\n\tif err := vm.Init(app.factory); err != nil {\n\t\tapp.Flash().Errf(\"Initializing the values model failed: %s\", err)\n\t}\n\n\ttoggleValuesCmd := func(*tcell.EventKey) *tcell.EventKey {\n\t\tif err := vm.ToggleValues(); err != nil {\n\t\t\tapp.Flash().Errf(\"Values toggle failed: %s\", err)\n\t\t\treturn nil\n\t\t}\n\n\t\tif err := vm.Refresh(ctx); err != nil {\n\t\t\tslog.Error(\"Values viewer refresh failed\", slogs.Error, err)\n\t\t\treturn nil\n\t\t}\n\n\t\tapp.Flash().Infof(\"Values toggled\")\n\t\treturn nil\n\t}\n\n\tv := NewLiveView(app, \"Values\", vm)\n\tv.actions.Add(ui.KeyV, ui.NewKeyAction(\"Toggle All Values\", toggleValuesCmd, true))\n\tif err := v.app.inject(v, false); err != nil {\n\t\tv.app.Flash().Err(err)\n\t}\n}\n"
  },
  {
    "path": "internal/view/vul_extender.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/tcell/v2\"\n)\n\n// VulnerabilityExtender adds vul image scan extensions.\ntype VulnerabilityExtender struct {\n\tResourceViewer\n}\n\n// NewVulnerabilityExtender returns a new extender.\nfunc NewVulnerabilityExtender(r ResourceViewer) ResourceViewer {\n\tv := VulnerabilityExtender{ResourceViewer: r}\n\tv.AddBindKeysFn(v.bindKeys)\n\n\treturn &v\n}\n\nfunc (v *VulnerabilityExtender) bindKeys(aa *ui.KeyActions) {\n\tif v.App().Config.K9s.ImageScans.Enable {\n\t\taa.Bulk(ui.KeyMap{\n\t\t\tui.KeyV:      ui.NewKeyAction(\"Show Vulnerabilities\", v.showVulCmd, true),\n\t\t\tui.KeyShiftV: ui.NewKeyAction(\"Sort Vulnerabilities\", v.GetTable().SortColCmd(\"VS\", true), false),\n\t\t})\n\t}\n}\n\nfunc (v *VulnerabilityExtender) showVulCmd(*tcell.EventKey) *tcell.EventKey {\n\tisv := NewImageScan(client.ScnGVR)\n\tisv.SetContextFn(v.selContext)\n\tif err := v.App().inject(isv, false); err != nil {\n\t\tv.App().Flash().Err(err)\n\t}\n\n\treturn nil\n}\n\nfunc (v *VulnerabilityExtender) selContext(ctx context.Context) context.Context {\n\tctx = context.WithValue(ctx, internal.KeyPath, v.GetTable().GetSelectedItem())\n\treturn context.WithValue(ctx, internal.KeyGVR, v.GVR())\n}\n"
  },
  {
    "path": "internal/view/workload.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/ui/dialog\"\n\t\"github.com/derailed/tcell/v2\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// Workload presents a workload viewer.\ntype Workload struct {\n\tResourceViewer\n}\n\n// NewWorkload returns a new viewer.\nfunc NewWorkload(gvr *client.GVR) ResourceViewer {\n\tw := Workload{\n\t\tResourceViewer: NewBrowser(gvr),\n\t}\n\tw.GetTable().SetEnterFn(w.showRes)\n\tw.AddBindKeysFn(w.bindKeys)\n\tw.GetTable().SetSortCol(\"KIND\", true)\n\n\treturn &w\n}\n\nfunc (w *Workload) bindDangerousKeys(aa *ui.KeyActions) {\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyE: ui.NewKeyActionWithOpts(\"Edit\", w.editCmd,\n\t\t\tui.ActionOpts{\n\t\t\t\tVisible:   true,\n\t\t\t\tDangerous: true,\n\t\t\t}),\n\t\ttcell.KeyCtrlD: ui.NewKeyActionWithOpts(\"Delete\", w.deleteCmd,\n\t\t\tui.ActionOpts{\n\t\t\t\tVisible:   true,\n\t\t\t\tDangerous: true,\n\t\t\t}),\n\t})\n}\n\nfunc (w *Workload) bindKeys(aa *ui.KeyActions) {\n\tif !w.App().Config.IsReadOnly() {\n\t\tw.bindDangerousKeys(aa)\n\t}\n\n\taa.Bulk(ui.KeyMap{\n\t\tui.KeyShiftK: ui.NewKeyAction(\"Sort Kind\", w.GetTable().SortColCmd(\"KIND\", true), false),\n\t\tui.KeyShiftR: ui.NewKeyAction(\"Sort Ready\", w.GetTable().SortColCmd(\"READY\", true), false),\n\t\tui.KeyShiftA: ui.NewKeyAction(\"Sort Age\", w.GetTable().SortColCmd(ageCol, true), false),\n\t\tui.KeyY:      ui.NewKeyAction(yamlAction, w.yamlCmd, true),\n\t\tui.KeyD:      ui.NewKeyAction(\"Describe\", w.describeCmd, true),\n\t})\n}\n\nfunc parsePath(path string) (*client.GVR, string, bool) {\n\ttt := strings.Split(path, \"|\")\n\tif len(tt) != 3 {\n\t\tslog.Error(\"Unable to parse workload path\", slogs.Path, path)\n\t\treturn client.NoGVR, client.FQN(\"\", \"\"), false\n\t}\n\n\treturn client.NewGVR(tt[0]), client.FQN(tt[1], tt[2]), true\n}\n\nfunc (*Workload) showRes(app *App, _ ui.Tabular, _ *client.GVR, path string) {\n\tgvr, fqn, ok := parsePath(path)\n\tif !ok {\n\t\tapp.Flash().Err(fmt.Errorf(\"unable to parse path: %q\", path))\n\t\treturn\n\t}\n\tapp.gotoResource(gvr.String(), fqn, false, true)\n}\n\nfunc (w *Workload) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tselections := w.GetTable().GetSelectedItems()\n\tif len(selections) == 0 {\n\t\treturn evt\n\t}\n\n\tw.Stop()\n\tdefer w.Start()\n\t{\n\t\tmsg := fmt.Sprintf(\"Delete %s %s?\", w.GVR().R(), selections[0])\n\t\tif len(selections) > 1 {\n\t\t\tmsg = fmt.Sprintf(\"Delete %d marked %s?\", len(selections), w.GVR())\n\t\t}\n\t\tw.resourceDelete(selections, msg)\n\t}\n\n\treturn nil\n}\n\nfunc (w *Workload) defaultContext(gvr *client.GVR, fqn string) context.Context {\n\tctx := context.WithValue(context.Background(), internal.KeyFactory, w.App().factory)\n\tctx = context.WithValue(ctx, internal.KeyGVR, gvr)\n\tif fqn != \"\" {\n\t\tctx = context.WithValue(ctx, internal.KeyPath, fqn)\n\t}\n\tif internal.IsLabelSelector(w.GetTable().CmdBuff().GetText()) {\n\t\tif sel, err := ui.ExtractLabelSelector(w.GetTable().CmdBuff().GetText()); err == nil {\n\t\t\tctx = context.WithValue(ctx, internal.KeyLabels, sel)\n\t\t}\n\t}\n\tctx = context.WithValue(ctx, internal.KeyNamespace, client.CleanseNamespace(w.App().Config.ActiveNamespace()))\n\tctx = context.WithValue(ctx, internal.KeyWithMetrics, w.App().factory.Client().HasMetrics())\n\n\treturn ctx\n}\n\nfunc (w *Workload) resourceDelete(selections []string, msg string) {\n\tokFn := func(propagation *metav1.DeletionPropagation, force bool) {\n\t\tw.GetTable().ShowDeleted()\n\t\tif len(selections) > 1 {\n\t\t\tw.App().Flash().Infof(\"Delete %d marked %s\", len(selections), w.GVR())\n\t\t} else {\n\t\t\tw.App().Flash().Infof(\"Delete resource %s %s\", w.GVR(), selections[0])\n\t\t}\n\t\tfor _, sel := range selections {\n\t\t\tgvr, fqn, ok := parsePath(sel)\n\t\t\tif !ok {\n\t\t\t\tw.App().Flash().Err(fmt.Errorf(\"unable to parse path: %q\", sel))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tgrace := dao.DefaultGrace\n\t\t\tif force {\n\t\t\t\tgrace = dao.ForceGrace\n\t\t\t}\n\t\t\tif err := w.GetTable().GetModel().Delete(w.defaultContext(gvr, fqn), fqn, propagation, grace); err != nil {\n\t\t\t\tw.App().Flash().Errf(\"Delete failed with `%s\", err)\n\t\t\t} else {\n\t\t\t\tw.App().factory.DeleteForwarder(sel)\n\t\t\t}\n\t\t\tw.GetTable().DeleteMark(sel)\n\t\t}\n\t\tw.GetTable().Start()\n\t}\n\td := w.App().Styles.Dialog()\n\tdialog.ShowDelete(&d, w.App().Content.Pages, msg, okFn, func() {})\n}\n\nfunc (w *Workload) describeCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := w.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\tgvr, fqn, ok := parsePath(path)\n\tif !ok {\n\t\tw.App().Flash().Err(fmt.Errorf(\"unable to parse path: %q\", path))\n\t\treturn evt\n\t}\n\n\tdescribeResource(w.App(), nil, gvr, fqn)\n\n\treturn nil\n}\n\nfunc (w *Workload) editCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := w.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\tgvr, fqn, ok := parsePath(path)\n\tif !ok {\n\t\tw.App().Flash().Err(fmt.Errorf(\"unable to parse path: %q\", path))\n\t\treturn evt\n\t}\n\n\tw.Stop()\n\tdefer w.Start()\n\tif err := editRes(w.App(), gvr, fqn); err != nil {\n\t\tw.App().Flash().Err(err)\n\t}\n\n\treturn nil\n}\n\nfunc (w *Workload) yamlCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tpath := w.GetTable().GetSelectedItem()\n\tif path == \"\" {\n\t\treturn evt\n\t}\n\tgvr, fqn, ok := parsePath(path)\n\tif !ok {\n\t\tw.App().Flash().Err(fmt.Errorf(\"unable to parse path: %q\", path))\n\t\treturn evt\n\t}\n\n\tv := NewLiveView(w.App(), yamlAction, model.NewYAML(gvr, fqn))\n\tif err := v.app.inject(v, false); err != nil {\n\t\tv.app.Flash().Err(err)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/view/xray.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/model\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/k9s/internal/ui\"\n\t\"github.com/derailed/k9s/internal/ui/dialog\"\n\t\"github.com/derailed/k9s/internal/view/cmd\"\n\t\"github.com/derailed/k9s/internal/xray\"\n\t\"github.com/derailed/tcell/v2\"\n\t\"github.com/derailed/tview\"\n\t\"github.com/sahilm/fuzzy\"\n\t\"golang.org/x/text/cases\"\n\t\"golang.org/x/text/language\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/util/sets\"\n)\n\nconst xrayTitle = \"Xray\"\n\nvar _ ResourceViewer = (*Xray)(nil)\n\n// Xray represents an xray tree view.\ntype Xray struct {\n\t*ui.Tree\n\n\tapp      *App\n\tgvr      *client.GVR\n\tmeta     *metav1.APIResource\n\tmodel    *model.Tree\n\tcancelFn context.CancelFunc\n\tenvFn    EnvFunc\n}\n\n// NewXray returns a new view.\nfunc NewXray(gvr *client.GVR) ResourceViewer {\n\treturn &Xray{\n\t\tgvr:   gvr,\n\t\tTree:  ui.NewTree(),\n\t\tmodel: model.NewTree(gvr),\n\t}\n}\n\nfunc (*Xray) SetCommand(*cmd.Interpreter)            {}\nfunc (*Xray) SetFilter(string, bool)                 {}\nfunc (*Xray) SetLabelSelector(labels.Selector, bool) {}\n\n// Init initializes the view.\nfunc (x *Xray) Init(ctx context.Context) error {\n\tx.envFn = x.k9sEnv\n\n\tif err := x.Tree.Init(ctx); err != nil {\n\t\treturn err\n\t}\n\tx.SetKeyListenerFn(x.keyEntered)\n\n\tvar err error\n\tx.meta, err = dao.MetaAccess.MetaFor(x.gvr)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif x.app, err = extractApp(ctx); err != nil {\n\t\treturn err\n\t}\n\n\tx.bindKeys()\n\tx.SetBackgroundColor(x.app.Styles.Xray().BgColor.Color())\n\tx.SetBorderColor(x.app.Styles.Xray().FgColor.Color())\n\tx.SetBorderFocusColor(x.app.Styles.Frame().Border.FocusColor.Color())\n\tx.SetGraphicsColor(x.app.Styles.Xray().GraphicColor.Color())\n\tx.SetTitle(fmt.Sprintf(\" %s-%s \", xrayTitle, cases.Title(language.Und, cases.NoLower).String(x.gvr.R())))\n\n\tx.model.SetRefreshRate(x.app.Config.K9s.RefreshDuration())\n\tx.model.SetNamespace(client.CleanseNamespace(x.app.Config.ActiveNamespace()))\n\tx.model.AddListener(x)\n\n\tx.SetChangedFunc(func(n *tview.TreeNode) {\n\t\tspec, ok := n.GetReference().(xray.NodeSpec)\n\t\tif !ok {\n\t\t\tslog.Error(\"No ref found on node\", slogs.FQN, n.GetText())\n\t\t\treturn\n\t\t}\n\t\tx.SetSelectedItem(spec.AsPath())\n\t\tx.refreshActions()\n\t})\n\tx.refreshActions()\n\n\treturn nil\n}\n\n// InCmdMode checks if prompt is active.\nfunc (*Xray) InCmdMode() bool {\n\treturn false\n}\n\n// ExtraHints returns additional hints.\nfunc (x *Xray) ExtraHints() map[string]string {\n\tif x.app.Config.K9s.UI.NoIcons {\n\t\treturn nil\n\t}\n\treturn xray.EmojiInfo()\n}\n\n// SetInstance sets specific resource instance.\nfunc (*Xray) SetInstance(string) {}\n\nfunc (x *Xray) bindKeys() {\n\tx.Actions().Bulk(ui.KeyMap{\n\t\tui.KeySlash:     ui.NewSharedKeyAction(\"Filter Mode\", x.activateCmd, false),\n\t\ttcell.KeyEscape: ui.NewSharedKeyAction(\"Filter Reset\", x.resetCmd, false),\n\t\ttcell.KeyEnter:  ui.NewKeyAction(\"Goto\", x.gotoCmd, true),\n\t})\n}\n\nfunc (x *Xray) keyEntered() {\n\tx.ClearSelection()\n\tx.update(x.filter(x.model.Peek()))\n}\n\nfunc (x *Xray) refreshActions() {\n\taa := ui.NewKeyActions()\n\n\tdefer func() {\n\t\tif err := pluginActions(x, aa); err != nil {\n\t\t\tslog.Warn(\"Plugins load failed\", slogs.Error, err)\n\t\t}\n\t\tif err := hotKeyActions(x, aa); err != nil {\n\t\t\tslog.Warn(\"HotKeys load failed\", slogs.Error, err)\n\t\t}\n\n\t\tx.Actions().Merge(aa)\n\t\tx.app.Menu().HydrateMenu(x.Hints())\n\t}()\n\n\tx.Actions().Clear()\n\tx.bindKeys()\n\tx.BindKeys()\n\n\tspec := x.selectedSpec()\n\tif spec == nil {\n\t\treturn\n\t}\n\n\tgvr := spec.GVR()\n\tvar err error\n\tx.meta, err = dao.MetaAccess.MetaFor(gvr)\n\tif err != nil {\n\t\tslog.Warn(\"No meta found!\",\n\t\t\tslogs.GVR, gvr,\n\t\t\tslogs.Error, err,\n\t\t)\n\t\treturn\n\t}\n\n\tif client.Can(x.meta.Verbs, \"edit\") {\n\t\taa.Add(ui.KeyE, ui.NewKeyAction(\"Edit\", x.editCmd, true))\n\t}\n\tif client.Can(x.meta.Verbs, \"delete\") {\n\t\taa.Add(tcell.KeyCtrlD, ui.NewKeyAction(\"Delete\", x.deleteCmd, true))\n\t}\n\tif !dao.IsK9sMeta(x.meta) {\n\t\taa.Bulk(ui.KeyMap{\n\t\t\tui.KeyY: ui.NewKeyAction(yamlAction, x.viewCmd, true),\n\t\t\tui.KeyD: ui.NewKeyAction(\"Describe\", x.describeCmd, true),\n\t\t})\n\t}\n\n\tswitch gvr {\n\tcase client.NsGVR:\n\t\tx.Actions().Delete(tcell.KeyEnter)\n\tcase client.CoGVR:\n\t\tx.Actions().Delete(tcell.KeyEnter)\n\t\taa.Bulk(ui.KeyMap{\n\t\t\tui.KeyS: ui.NewKeyAction(\"Shell\", x.shellCmd, true),\n\t\t\tui.KeyL: ui.NewKeyAction(\"Logs\", x.logsCmd(false), true),\n\t\t\tui.KeyP: ui.NewKeyAction(\"Logs Previous\", x.logsCmd(true), true),\n\t\t})\n\tcase client.PodGVR:\n\t\taa.Bulk(ui.KeyMap{\n\t\t\tui.KeyS: ui.NewKeyAction(\"Shell\", x.shellCmd, true),\n\t\t\tui.KeyA: ui.NewKeyAction(\"Attach\", x.attachCmd, true),\n\t\t\tui.KeyL: ui.NewKeyAction(\"Logs\", x.logsCmd(false), true),\n\t\t\tui.KeyP: ui.NewKeyAction(\"Logs Previous\", x.logsCmd(true), true),\n\t\t})\n\t}\n\tx.Actions().Merge(aa)\n}\n\n// GetSelectedPath returns the current selection as string.\nfunc (x *Xray) GetSelectedPath() string {\n\tspec := x.selectedSpec()\n\tif spec == nil {\n\t\treturn \"\"\n\t}\n\treturn spec.Path()\n}\n\nfunc (x *Xray) selectedSpec() *xray.NodeSpec {\n\tnode := x.GetCurrentNode()\n\tif node == nil {\n\t\treturn nil\n\t}\n\n\tref, ok := node.GetReference().(xray.NodeSpec)\n\tif !ok {\n\t\tslog.Error(\"Expecting a NodeSpec\",\n\t\t\tslogs.Path, node.GetText(),\n\t\t\tslogs.RefType, fmt.Sprintf(\"%T\", node.GetReference()),\n\t\t)\n\t\treturn nil\n\t}\n\n\treturn &ref\n}\n\n// EnvFn returns an plugin env function if available.\nfunc (x *Xray) EnvFn() EnvFunc {\n\treturn x.envFn\n}\n\nfunc (x *Xray) k9sEnv() Env {\n\tenv := k8sEnv(x.app.Conn().Config())\n\n\tspec := x.selectedSpec()\n\tif spec == nil {\n\t\treturn env\n\t}\n\n\tenv[\"FILTER\"] = x.CmdBuff().GetText()\n\tif env[\"FILTER\"] == \"\" {\n\t\tns, n := client.Namespaced(spec.Path())\n\t\tenv[\"NAMESPACE\"], env[\"FILTER\"] = ns, n\n\t}\n\n\tswitch spec.GVR() {\n\tcase client.CoGVR:\n\t\t_, co := client.Namespaced(spec.Path())\n\t\tenv[\"CONTAINER\"] = co\n\t\tns, n := client.Namespaced(*spec.ParentPath())\n\t\tenv[\"NAMESPACE\"], env[\"POD\"], env[\"NAME\"] = ns, n, co\n\tdefault:\n\t\tns, n := client.Namespaced(spec.Path())\n\t\tenv[\"NAMESPACE\"], env[\"NAME\"] = ns, n\n\t}\n\n\treturn env\n}\n\n// Aliases returns all available aliases.\nfunc (x *Xray) Aliases() sets.Set[string] {\n\treturn aliases(x.meta, x.app.command.AliasesFor(client.NewGVRFromMeta(x.meta)))\n}\n\nfunc (x *Xray) logsCmd(prev bool) func(evt *tcell.EventKey) *tcell.EventKey {\n\treturn func(*tcell.EventKey) *tcell.EventKey {\n\t\tspec := x.selectedSpec()\n\t\tif spec == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tx.showLogs(spec, prev)\n\n\t\treturn nil\n\t}\n}\n\nfunc (x *Xray) showLogs(spec *xray.NodeSpec, prev bool) {\n\t// Need to load and wait for pods\n\tpath, co := spec.Path(), \"\"\n\tif spec.GVR() == client.CoGVR {\n\t\t_, coName := client.Namespaced(spec.Path())\n\t\tpath, co = *spec.ParentPath(), coName\n\t}\n\n\tns, _ := client.Namespaced(path)\n\t_, err := x.app.factory.CanForResource(ns, client.PodGVR, client.ListAccess)\n\tif err != nil {\n\t\tx.app.Flash().Err(err)\n\t\treturn\n\t}\n\n\topts := dao.LogOptions{\n\t\tPath:      path,\n\t\tContainer: co,\n\t\tPrevious:  prev,\n\t}\n\tif err := x.app.inject(NewLog(client.PodGVR, &opts), false); err != nil {\n\t\tx.app.Flash().Err(err)\n\t}\n}\n\nfunc (x *Xray) shellCmd(*tcell.EventKey) *tcell.EventKey {\n\tspec := x.selectedSpec()\n\tif spec == nil {\n\t\treturn nil\n\t}\n\n\tif spec.Status() != \"ok\" {\n\t\tx.app.Flash().Errf(\"%s is not in a running state\", spec.Path())\n\t\treturn nil\n\t}\n\n\tpath, co := spec.Path(), \"\"\n\tif spec.GVR() == client.CoGVR {\n\t\t_, co = client.Namespaced(spec.Path())\n\t\tpath = *spec.ParentPath()\n\t}\n\n\tif err := containerShellIn(x.app, x, path, co); err != nil {\n\t\tx.app.Flash().Err(err)\n\t}\n\n\treturn nil\n}\n\nfunc (x *Xray) attachCmd(*tcell.EventKey) *tcell.EventKey {\n\tspec := x.selectedSpec()\n\tif spec == nil {\n\t\treturn nil\n\t}\n\n\tif spec.Status() != \"ok\" {\n\t\tx.app.Flash().Errf(\"%s is not in a running state\", spec.Path())\n\t\treturn nil\n\t}\n\n\tpath, co := spec.Path(), \"\"\n\tif spec.GVR() == client.CoGVR {\n\t\tpath = *spec.ParentPath()\n\t}\n\n\tif err := containerAttachIn(x.app, x, path, co); err != nil {\n\t\tx.app.Flash().Err(err)\n\t}\n\n\treturn nil\n}\n\nfunc (x *Xray) viewCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tspec := x.selectedSpec()\n\tif spec == nil {\n\t\treturn evt\n\t}\n\n\tctx := x.defaultContext()\n\traw, err := x.model.ToYAML(ctx, spec.GVR(), spec.Path())\n\tif err != nil {\n\t\tx.App().Flash().Errf(\"unable to get resource %q -- %s\", spec.GVR(), err)\n\t\treturn nil\n\t}\n\n\tdetails := NewDetails(x.app, yamlAction, spec.Path(), contentYAML, true).Update(raw)\n\tif err := x.app.inject(details, false); err != nil {\n\t\tx.app.Flash().Err(err)\n\t}\n\n\treturn nil\n}\n\nfunc (x *Xray) deleteCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tspec := x.selectedSpec()\n\tif spec == nil {\n\t\treturn evt\n\t}\n\n\tx.Stop()\n\tdefer x.Start()\n\t{\n\t\tmeta, err := dao.MetaAccess.MetaFor(spec.GVR())\n\t\tif err != nil {\n\t\t\tslog.Warn(\"No meta found!\",\n\t\t\t\tslogs.GVR, spec.GVR(),\n\t\t\t\tslogs.Error, err,\n\t\t\t)\n\t\t\treturn nil\n\t\t}\n\t\tx.resourceDelete(spec.GVR(), spec, fmt.Sprintf(\"Delete %s %s?\", meta.SingularName, spec.Path()))\n\t}\n\n\treturn nil\n}\n\nfunc (x *Xray) describeCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tspec := x.selectedSpec()\n\tif spec == nil {\n\t\treturn evt\n\t}\n\n\tx.describe(spec.GVR(), spec.Path())\n\n\treturn nil\n}\n\nfunc (x *Xray) describe(gvr *client.GVR, path string) {\n\tctx := context.Background()\n\tctx = context.WithValue(ctx, internal.KeyFactory, x.app.factory)\n\n\tyaml, err := x.model.Describe(ctx, gvr, path)\n\tif err != nil {\n\t\tx.app.Flash().Errf(\"Describe command failed: %s\", err)\n\t\treturn\n\t}\n\n\tdetails := NewDetails(x.app, \"Describe\", path, contentYAML, true).Update(yaml)\n\tif err := x.app.inject(details, false); err != nil {\n\t\tx.app.Flash().Err(err)\n\t}\n}\n\nfunc (x *Xray) editCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tspec := x.selectedSpec()\n\tif spec == nil {\n\t\treturn evt\n\t}\n\n\tx.Stop()\n\tdefer x.Start()\n\t{\n\t\tns, n := client.Namespaced(spec.Path())\n\t\targs := make([]string, 0, 10)\n\t\targs = append(args,\n\t\t\t\"edit\",\n\t\t\tspec.GVR().R(),\n\t\t\t\"-n\", ns,\n\t\t\t\"--context\", x.app.Config.K9s.ActiveContextName(),\n\t\t)\n\t\tif cfg := x.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != \"\" {\n\t\t\targs = append(args, \"--kubeconfig\", *cfg)\n\t\t}\n\t\tif err := runK(x.app, &shellOpts{args: append(args, n)}); err != nil {\n\t\t\tx.app.Flash().Errf(\"Edit exec failed: %s\", err)\n\t\t}\n\t}\n\n\treturn evt\n}\n\nfunc (x *Xray) activateCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif x.app.InCmdMode() {\n\t\treturn evt\n\t}\n\tx.app.ResetPrompt(x.CmdBuff())\n\n\treturn nil\n}\n\nfunc (x *Xray) resetCmd(evt *tcell.EventKey) *tcell.EventKey {\n\tif !x.CmdBuff().InCmdMode() {\n\t\tx.CmdBuff().Reset()\n\t\treturn x.app.PrevCmd(evt)\n\t}\n\tx.CmdBuff().Reset()\n\tx.model.ClearFilter()\n\tx.Start()\n\n\treturn nil\n}\n\nfunc (x *Xray) gotoCmd(*tcell.EventKey) *tcell.EventKey {\n\tif x.CmdBuff().IsActive() {\n\t\tif internal.IsLabelSelector(x.CmdBuff().GetText()) {\n\t\t\tx.Start()\n\t\t}\n\t\tx.CmdBuff().SetActive(false)\n\t\tx.GetRoot().ExpandAll()\n\n\t\treturn nil\n\t}\n\n\tspec := x.selectedSpec()\n\tif spec == nil {\n\t\treturn nil\n\t}\n\tif len(strings.Split(spec.Path(), \"/\")) == 1 {\n\t\treturn nil\n\t}\n\tx.app.gotoResource(spec.GVR().String(), spec.Path(), false, true)\n\n\treturn nil\n}\n\nfunc (x *Xray) filter(root *xray.TreeNode) *xray.TreeNode {\n\tq := x.CmdBuff().GetText()\n\tif x.CmdBuff().Empty() || internal.IsLabelSelector(q) {\n\t\treturn root\n\t}\n\n\tx.UpdateTitle()\n\tif f, ok := internal.IsFuzzySelector(q); ok {\n\t\treturn root.Filter(f, fuzzyFilter)\n\t}\n\n\tif internal.IsInverseSelector(q) {\n\t\treturn root.Filter(q, rxInverseFilter)\n\t}\n\n\treturn root.Filter(q, rxFilter)\n}\n\n// TreeNodeSelected callback for node selection.\nfunc (x *Xray) TreeNodeSelected() {\n\tx.app.QueueUpdateDraw(func() {\n\t\tn := x.GetCurrentNode()\n\t\tif n != nil {\n\t\t\tn.SetColor(x.app.Styles.Xray().CursorColor.Color())\n\t\t}\n\t})\n}\n\n// TreeLoadFailed notifies the load failed.\nfunc (x *Xray) TreeLoadFailed(err error) {\n\tx.app.Flash().Err(err)\n}\n\nfunc (x *Xray) update(node *xray.TreeNode) {\n\troot := makeTreeNode(node, x.ExpandNodes(), x.app.Config.K9s.UI.NoIcons, x.app.Styles)\n\tif node == nil {\n\t\tx.app.QueueUpdateDraw(func() {\n\t\t\tx.SetRoot(root)\n\t\t})\n\t\treturn\n\t}\n\n\tfor _, c := range node.Children {\n\t\tx.hydrate(root, c)\n\t}\n\tif x.GetSelectedItem() == \"\" {\n\t\tx.SetSelectedItem(node.Spec().Path())\n\t}\n\n\tx.app.QueueUpdateDraw(func() {\n\t\tx.SetRoot(root)\n\t\troot.Walk(func(node, parent *tview.TreeNode) bool {\n\t\t\tspec, ok := node.GetReference().(xray.NodeSpec)\n\t\t\tif !ok {\n\t\t\t\tslog.Error(\"Expecting a NodeSpec\",\n\t\t\t\t\tslogs.FQN, node.GetText(),\n\t\t\t\t\tslogs.RefType, fmt.Sprintf(\"%T\", node.GetReference()),\n\t\t\t\t)\n\t\t\t\treturn false\n\t\t\t}\n\t\t\t// BOZO!! Figure this out expand/collapse but the root\n\t\t\tif parent != nil {\n\t\t\t\tnode.SetExpanded(x.ExpandNodes())\n\t\t\t} else {\n\t\t\t\tnode.SetExpanded(true)\n\t\t\t}\n\n\t\t\tif spec.AsPath() == x.GetSelectedItem() {\n\t\t\t\tnode.SetExpanded(true).SetSelectable(true)\n\t\t\t\tx.SetCurrentNode(node)\n\t\t\t}\n\t\t\treturn true\n\t\t})\n\t})\n}\n\n// TreeChanged notifies the model data changed.\nfunc (x *Xray) TreeChanged(node *xray.TreeNode) {\n\tx.Count = node.Count(x.gvr)\n\tx.update(x.filter(node))\n\tx.UpdateTitle()\n}\n\nfunc (x *Xray) hydrate(parent *tview.TreeNode, n *xray.TreeNode) {\n\tnode := makeTreeNode(n, x.ExpandNodes(), x.app.Config.K9s.UI.NoIcons, x.app.Styles)\n\tfor _, c := range n.Children {\n\t\tx.hydrate(node, c)\n\t}\n\tparent.AddChild(node)\n}\n\n// SetEnvFn sets the custom environment function.\nfunc (*Xray) SetEnvFn(EnvFunc) {}\n\n// Refresh updates the view.\nfunc (*Xray) Refresh() {}\n\n// BufferCompleted indicates the buffer was changed.\nfunc (x *Xray) BufferCompleted(_, _ string) {\n\tx.update(x.filter(x.model.Peek()))\n}\n\n// BufferChanged indicates the buffer was changed.\nfunc (*Xray) BufferChanged(_, _ string) {}\n\n// BufferActive indicates the buff activity changed.\nfunc (x *Xray) BufferActive(state bool, k model.BufferKind) {\n\tx.app.BufferActive(state, k)\n}\n\nfunc (x *Xray) defaultContext() context.Context {\n\tctx := context.WithValue(context.Background(), internal.KeyFactory, x.app.factory)\n\tctx = context.WithValue(ctx, internal.KeyFields, \"\")\n\tif x.CmdBuff().Empty() {\n\t\tctx = context.WithValue(ctx, internal.KeyLabels, labels.Everything())\n\t} else {\n\t\tif sel, err := ui.ExtractLabelSelector(x.CmdBuff().GetText()); err == nil {\n\t\t\tctx = context.WithValue(ctx, internal.KeyLabels, sel)\n\t\t}\n\t}\n\n\treturn ctx\n}\n\n// Start initializes resource watch loop.\nfunc (x *Xray) Start() {\n\tx.Stop()\n\tx.CmdBuff().AddListener(x)\n\n\tctx := x.defaultContext()\n\tctx, x.cancelFn = context.WithCancel(ctx)\n\tx.model.Watch(ctx)\n\tx.UpdateTitle()\n}\n\n// Stop terminates watch loop.\nfunc (x *Xray) Stop() {\n\tif x.cancelFn == nil {\n\t\treturn\n\t}\n\tx.cancelFn()\n\tx.cancelFn = nil\n\tx.CmdBuff().RemoveListener(x)\n}\n\n// AddBindKeysFn sets up extra key bindings.\nfunc (*Xray) AddBindKeysFn(BindKeysFunc) {}\n\n// SetContextFn sets custom context.\nfunc (*Xray) SetContextFn(ContextFunc) {}\n\n// Name returns the component name.\nfunc (*Xray) Name() string { return \"XRay\" }\n\n// GetTable returns the underlying table.\nfunc (*Xray) GetTable() *Table { return nil }\n\n// GVR returns a resource descriptor.\nfunc (x *Xray) GVR() *client.GVR { return x.gvr }\n\n// App returns the current app handle.\nfunc (x *Xray) App() *App {\n\treturn x.app\n}\n\n// UpdateTitle updates the view title.\nfunc (x *Xray) UpdateTitle() {\n\tt := x.styleTitle()\n\tx.app.QueueUpdateDraw(func() {\n\t\tx.SetTitle(t)\n\t})\n}\n\nfunc (x *Xray) styleTitle() string {\n\tbase := fmt.Sprintf(\"%s-%s\", xrayTitle, cases.Title(language.Und, cases.NoLower).String(x.gvr.R()))\n\tns := x.model.GetNamespace()\n\tif client.IsAllNamespaces(ns) {\n\t\tns = client.NamespaceAll\n\t}\n\n\tvar (\n\t\ttitle  string\n\t\tstyles = x.app.Styles.Frame()\n\t)\n\tif ns == client.ClusterScope {\n\t\ttitle = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, render.AsThousands(int64(x.Count))), &styles)\n\t} else {\n\t\ttitle = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, render.AsThousands(int64(x.Count))), &styles)\n\t}\n\n\tbuff := x.CmdBuff().GetText()\n\tif buff == \"\" {\n\t\treturn title\n\t}\n\tif internal.IsLabelSelector(buff) {\n\t\tif sel, err := ui.ExtractLabelSelector(buff); err == nil {\n\t\t\tbuff = sel.String()\n\t\t}\n\t}\n\n\treturn title + ui.SkinTitle(fmt.Sprintf(ui.SearchFmt, buff), &styles)\n}\n\nfunc (x *Xray) resourceDelete(gvr *client.GVR, spec *xray.NodeSpec, msg string) {\n\td := x.app.Styles.Dialog()\n\tdialog.ShowDelete(&d, x.app.Content.Pages, msg, func(_ *metav1.DeletionPropagation, force bool) {\n\t\tx.app.Flash().Infof(\"Delete resource %s %s\", spec.GVR(), spec.Path())\n\t\taccessor, err := dao.AccessorFor(x.app.factory, gvr)\n\t\tif err != nil {\n\t\t\tslog.Error(\"No accessor found\",\n\t\t\t\tslogs.GVR, gvr,\n\t\t\t\tslogs.Error, err,\n\t\t\t)\n\t\t\treturn\n\t\t}\n\n\t\tnuker, ok := accessor.(dao.Nuker)\n\t\tif !ok {\n\t\t\tx.app.Flash().Errf(\"Invalid nuker %T\", accessor)\n\t\t\treturn\n\t\t}\n\t\tgrace := dao.DefaultGrace\n\t\tif force {\n\t\t\tgrace = dao.ForceGrace\n\t\t}\n\t\tif err := nuker.Delete(context.Background(), spec.Path(), nil, grace); err != nil {\n\t\t\tx.app.Flash().Errf(\"Delete failed with `%s\", err)\n\t\t} else {\n\t\t\tx.app.Flash().Infof(\"%s `%s deleted successfully\", x.GVR(), spec.Path())\n\t\t\tx.app.factory.DeleteForwarder(spec.Path())\n\t\t}\n\t\tx.Refresh()\n\t}, func() {})\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc fuzzyFilter(q, path string) bool {\n\tq = strings.TrimSpace(q[2:])\n\tmm := fuzzy.Find(q, []string{path})\n\n\treturn len(mm) > 0\n}\n\nfunc rxFilter(q, path string) bool {\n\trx := regexp.MustCompile(`(?i)` + q)\n\ttokens := strings.Split(path, xray.PathSeparator)\n\tfor _, t := range tokens {\n\t\tif rx.MatchString(t) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc rxInverseFilter(q, path string) bool {\n\tq = strings.TrimSpace(q[1:])\n\trx := regexp.MustCompile(`(?i)` + q)\n\ttokens := strings.Split(path, xray.PathSeparator)\n\tfor _, t := range tokens {\n\t\tif rx.MatchString(t) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc makeTreeNode(node *xray.TreeNode, expanded, showIcons bool, styles *config.Styles) *tview.TreeNode {\n\tn := tview.NewTreeNode(\"No data...\")\n\tif node != nil {\n\t\tn.SetText(node.Title(showIcons))\n\t\tn.SetReference(node.Spec())\n\t}\n\tn.SetSelectable(true)\n\tn.SetExpanded(expanded)\n\tn.SetColor(styles.Xray().CursorColor.Color())\n\tn.SetSelectedFunc(func() {\n\t\tn.SetExpanded(!n.IsExpanded())\n\t})\n\treturn n\n}\n"
  },
  {
    "path": "internal/view/yaml.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/config/data\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"github.com/derailed/tview\"\n)\n\nvar (\n\tkeyValRX = regexp.MustCompile(`\\A(\\s*)([\\w\\-./\\s]+):\\s(.+)\\z`)\n\tkeyRX    = regexp.MustCompile(`\\A(\\s*)([\\w\\-./\\s]+):\\s*\\z`)\n\tsearchRX = regexp.MustCompile(`<<<(\"search_\\d+\")>>>(.+)<<<\"\">>>`)\n)\n\nconst (\n\tyamlFullFmt  = \"%s[key::b]%s[colon::-]: [val::]%s\"\n\tyamlKeyFmt   = \"%s[key::b]%s[colon::-]:\"\n\tyamlValueFmt = \"[val::]%s\"\n)\n\nfunc colorizeYAML(style config.Yaml, raw string) string {\n\tlines := strings.Split(tview.Escape(raw), \"\\n\")\n\tfullFmt := strings.Replace(yamlFullFmt, \"[key\", \"[\"+style.KeyColor.String(), 1)\n\tfullFmt = strings.Replace(fullFmt, \"[colon\", \"[\"+style.ColonColor.String(), 1)\n\tfullFmt = strings.Replace(fullFmt, \"[val\", \"[\"+style.ValueColor.String(), 1)\n\n\tkeyFmt := strings.Replace(yamlKeyFmt, \"[key\", \"[\"+style.KeyColor.String(), 1)\n\tkeyFmt = strings.Replace(keyFmt, \"[colon\", \"[\"+style.ColonColor.String(), 1)\n\n\tvalFmt := strings.Replace(yamlValueFmt, \"[val\", \"[\"+style.ValueColor.String(), 1)\n\n\tbuff := make([]string, 0, len(lines))\n\tfor _, l := range lines {\n\t\tres := keyValRX.FindStringSubmatch(l)\n\t\tif len(res) == 4 {\n\t\t\tbuff = append(buff, enableRegion(fmt.Sprintf(fullFmt, res[1], res[2], res[3])))\n\t\t\tcontinue\n\t\t}\n\n\t\tres = keyRX.FindStringSubmatch(l)\n\t\tif len(res) == 3 {\n\t\t\tbuff = append(buff, enableRegion(fmt.Sprintf(keyFmt, res[1], res[2])))\n\t\t\tcontinue\n\t\t}\n\n\t\tbuff = append(buff, enableRegion(fmt.Sprintf(valFmt, l)))\n\t}\n\n\treturn strings.Join(buff, \"\\n\")\n}\n\nfunc enableRegion(s string) string {\n\tif searchRX.MatchString(s) {\n\t\treturn strings.ReplaceAll(strings.ReplaceAll(s, \"<<<\", \"[\"), \">>>\", \"]\")\n\t}\n\n\treturn s\n}\n\nfunc saveYAML(dir, name, raw string) (string, error) {\n\tif err := ensureDir(dir); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfName := fmt.Sprintf(\"%s--%d.yaml\", data.SanitizeFileName(name), time.Now().Unix())\n\tfpath := filepath.Join(dir, fName)\n\tmod := os.O_CREATE | os.O_WRONLY\n\tfile, err := os.OpenFile(fpath, mod, 0600)\n\tif err != nil {\n\t\tslog.Error(\"Unable to open YAML file\",\n\t\t\tslogs.Path, fpath,\n\t\t\tslogs.Error, err,\n\t\t)\n\t\treturn \"\", nil\n\t}\n\tdefer func() {\n\t\tif err := file.Close(); err != nil {\n\t\t\tslog.Error(\"Closing yaml file failed\",\n\t\t\t\tslogs.Path, fpath,\n\t\t\t\tslogs.Error, err,\n\t\t\t)\n\t\t}\n\t}()\n\tif _, err := file.WriteString(raw); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn fpath, nil\n}\n"
  },
  {
    "path": "internal/view/yaml_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage view\n\nimport (\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestYaml(t *testing.T) {\n\tuu := []struct {\n\t\ts, e string\n\t}{\n\t\t{\n\t\t\t`api: fred\n\t\t   version: v1`,\n\t\t\t`[#4682b4::b]api[#ffffff::-]: [#ffefd5::]fred\n\t\t   [#4682b4::b]version[#ffffff::-]: [#ffefd5::]v1`,\n\t\t},\n\t\t{\n\t\t\t`api: <<<\"search_0\">>>fred<<<\"\">>>\n\t\t   version: v1`,\n\t\t\t`[#4682b4::b]api[#ffffff::-]: [#ffefd5::][\"search_0\"]fred[\"\"]\n\t\t   [#4682b4::b]version[#ffffff::-]: [#ffefd5::]v1`,\n\t\t},\n\t\t{\n\t\t\t`api:\n\t\t\tversion: v1`,\n\t\t\t`[#4682b4::b]api[#ffffff::-]:\n\t\t\t[#4682b4::b]version[#ffffff::-]: [#ffefd5::]v1`,\n\t\t},\n\t\t{\n\t\t\t\"      fred:blee\",\n\t\t\t\"[#ffefd5::]      fred:blee\",\n\t\t},\n\t\t{\n\t\t\t\"fred blee: blee\",\n\t\t\t\"[#4682b4::b]fred blee[#ffffff::-]: [#ffefd5::]blee\",\n\t\t},\n\t\t{\n\t\t\t\"Node-Selectors:  <none>\",\n\t\t\t\"[#4682b4::b]Node-Selectors[#ffffff::-]: [#ffefd5::] <none>\",\n\t\t},\n\t\t{\n\t\t\t\"fred.blee:  <none>\",\n\t\t\t\"[#4682b4::b]fred.blee[#ffffff::-]: [#ffefd5::] <none>\",\n\t\t},\n\t\t{\n\t\t\t\"certmanager.k8s.io/cluster-issuer: nameOfClusterIssuer\",\n\t\t\t\"[#4682b4::b]certmanager.k8s.io/cluster-issuer[#ffffff::-]: [#ffefd5::]nameOfClusterIssuer\",\n\t\t},\n\t\t{\n\t\t\t\"Message: Pod The node was low on resource: [DiskPressure].\",\n\t\t\t\"[#4682b4::b]Message[#ffffff::-]: [#ffefd5::]Pod The node was low on resource: [DiskPressure[].\",\n\t\t},\n\t\t{\n\t\t\t`data: \"<<<\"`,\n\t\t\t`[#4682b4::b]data[#ffffff::-]: [#ffefd5::]\"<<<\"`,\n\t\t},\n\t}\n\n\ts := config.NewStyles()\n\tfor _, u := range uu {\n\t\tassert.Equal(t, u.e, colorizeYAML(s.Views().Yaml, u.s))\n\t}\n}\n"
  },
  {
    "path": "internal/vul/scan.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage vul\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n)\n\nconst (\n\twontFix = \"(won't fix)\"\n\tnaValue = \"\"\n)\n\n// Scans tracks scans per image.\ntype Scans map[string]*Scan\n\n// Dump dumps reports to writer.\nfunc (s Scans) Dump(w io.Writer) error {\n\tfor k, v := range s {\n\t\t_, _ = fmt.Fprintf(w, \"Image: %s -- \", k)\n\t\tv.Tally.Dump(w)\n\t\t_, _ = fmt.Fprintln(w)\n\t\tif err := v.Dump(w); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Scan tracks image vulnerability scan.\ntype Scan struct {\n\tID    string\n\tTable *table\n\tTally tally\n}\n\nfunc newScan(img string) *Scan {\n\treturn &Scan{ID: img, Table: newTable()}\n}\n\n// Dump dump report to stdout.\nfunc (s *Scan) Dump(w io.Writer) error {\n\treturn s.Table.dump(w)\n}\n\nfunc (s *Scan) run(mm *match.Matches, store vulnerability.MetadataProvider) error {\n\tfor m := range mm.Enumerate() {\n\t\tmeta, err := store.VulnerabilityMetadata(vulnerability.Reference{\n\t\t\tID:        m.Vulnerability.ID,\n\t\t\tNamespace: m.Vulnerability.Namespace,\n\t\t})\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar severity string\n\t\tif meta != nil {\n\t\t\tseverity = meta.Severity\n\t\t}\n\t\tfixVersion := strings.Join(m.Vulnerability.Fix.Versions, \", \")\n\t\tswitch m.Vulnerability.Fix.State {\n\t\tcase vulnerability.FixStateWontFix:\n\t\t\tfixVersion = wontFix\n\t\tcase vulnerability.FixStateUnknown:\n\t\t\tfixVersion = naValue\n\t\t}\n\t\ts.Table.addRow(newRow(m.Package.Name, m.Package.Version, fixVersion, string(m.Package.Type), m.Vulnerability.ID, severity))\n\t}\n\ts.Table.dedup()\n\ts.Tally = newTally(s.Table)\n\n\treturn nil\n}\n\nfunc colorize(rr []string) []string {\n\tcrr := make([]string, len(rr))\n\tcopy(crr, rr)\n\n\tcrr[len(crr)-1] = sevColor(crr[len(crr)-1])\n\treturn crr\n}\n"
  },
  {
    "path": "internal/vul/scanner.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage vul\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/anchore/clio\"\n\t\"github.com/anchore/grype/cmd/grype/cli/options\"\n\t\"github.com/anchore/grype/grype\"\n\t\"github.com/anchore/grype/grype/match\"\n\t\"github.com/anchore/grype/grype/matcher\"\n\t\"github.com/anchore/grype/grype/matcher/dotnet\"\n\t\"github.com/anchore/grype/grype/matcher/golang\"\n\t\"github.com/anchore/grype/grype/matcher/java\"\n\t\"github.com/anchore/grype/grype/matcher/javascript\"\n\t\"github.com/anchore/grype/grype/matcher/python\"\n\t\"github.com/anchore/grype/grype/matcher/ruby\"\n\t\"github.com/anchore/grype/grype/matcher/stock\"\n\t\"github.com/anchore/grype/grype/pkg\"\n\t\"github.com/anchore/grype/grype/vex\"\n\t\"github.com/anchore/grype/grype/vulnerability\"\n\t\"github.com/anchore/syft/syft\"\n\t\"github.com/derailed/k9s/internal/config\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n)\n\nvar ImgScanner *imageScanner\n\nconst (\n\timgChanSize     = 3\n\timgScanTimeout  = 2 * time.Second\n\tscanConcurrency = 2\n)\n\ntype imageScanner struct {\n\tprovider    vulnerability.Provider\n\tstatus      *vulnerability.ProviderStatus\n\topts        *options.Grype\n\tscans       Scans\n\tmx          sync.RWMutex\n\tinitialized bool\n\tconfig      config.ImageScans\n\tlog         *slog.Logger\n}\n\n// NewImageScanner returns a new instance.\nfunc NewImageScanner(cfg config.ImageScans, l *slog.Logger) *imageScanner {\n\treturn &imageScanner{\n\t\tscans:  make(Scans),\n\t\tconfig: cfg,\n\t\tlog:    l.With(slogs.Subsys, \"vul\"),\n\t}\n}\n\nfunc (s *imageScanner) ShouldExcludes(ns string, lbls map[string]string) bool {\n\treturn s.config.ShouldExclude(ns, lbls)\n}\n\n// GetScan fetch scan for a given image. Returns ok=false when not found.\nfunc (s *imageScanner) GetScan(img string) (*Scan, bool) {\n\ts.mx.RLock()\n\tdefer s.mx.RUnlock()\n\n\tscan, ok := s.scans[img]\n\n\treturn scan, ok\n}\n\nfunc (s *imageScanner) setScan(img string, sc *Scan) {\n\ts.mx.Lock()\n\tdefer s.mx.Unlock()\n\n\ts.scans[img] = sc\n}\n\n// Init initializes image vulnerability database.\nfunc (s *imageScanner) Init(name, version string) {\n\tdefer func(t time.Time) {\n\t\tslog.Debug(\"VulDb initialization complete\",\n\t\t\tslogs.Elapsed, time.Since(t),\n\t\t)\n\t}(time.Now())\n\n\topts := options.DefaultGrype(clio.Identification{Name: name, Version: version})\n\topts.GenerateMissingCPEs = true\n\n\tprovider, status, err := grype.LoadVulnerabilityDB(\n\t\topts.ToClientConfig(),\n\t\topts.ToCuratorConfig(),\n\t\topts.DB.AutoUpdate,\n\t)\n\tif err != nil {\n\t\ts.log.Error(\"VulDb load failed\", slogs.Error, err)\n\t\treturn\n\t}\n\ts.mx.Lock()\n\ts.opts, s.provider, s.status = opts, provider, status\n\ts.mx.Unlock()\n\n\tif e := validateDBLoad(err, status); e != nil {\n\t\ts.log.Error(\"VulDb validate failed\", slogs.Error, e)\n\t\treturn\n\t}\n\n\ts.mx.Lock()\n\ts.initialized = true\n\ts.mx.Unlock()\n\tslog.Debug(\"VulDB initialized\")\n}\n\n// Stop closes scan database.\nfunc (s *imageScanner) Stop() {\n\ts.mx.RLock()\n\tdefer s.mx.RUnlock()\n\n\tif s.provider != nil {\n\t\t_ = s.provider.Close()\n\t\ts.provider = nil\n\t}\n}\n\nfunc (s *imageScanner) Score(ii ...string) string {\n\tvar sc scorer\n\tfor _, i := range ii {\n\t\tif scan, ok := s.GetScan(i); ok {\n\t\t\tsc = sc.Add(newScorer(scan.Tally))\n\t\t}\n\t}\n\n\treturn sc.String()\n}\n\nfunc (s *imageScanner) IsInitialized() bool {\n\ts.mx.RLock()\n\tdefer s.mx.RUnlock()\n\n\treturn s.initialized\n}\n\nfunc (s *imageScanner) Enqueue(ctx context.Context, images ...string) {\n\tctx, cancel := context.WithTimeout(ctx, imgScanTimeout)\n\tdefer cancel()\n\n\tfor _, img := range images {\n\t\tif _, ok := s.GetScan(img); ok {\n\t\t\tcontinue\n\t\t}\n\t\tgo s.scanWorker(ctx, img)\n\t}\n}\n\nfunc (s *imageScanner) scanWorker(ctx context.Context, img string) {\n\tdefer s.log.Debug(\"ScanWorker bailing out!\")\n\n\ts.log.Debug(\"ScanWorker processing image\", slogs.Image, img)\n\tsc := newScan(img)\n\ts.setScan(img, sc)\n\tif err := s.scan(ctx, img, sc); err != nil {\n\t\ts.log.Warn(\"Scan failed for image\",\n\t\t\tslogs.Image, img,\n\t\t\tslogs.Error, err,\n\t\t)\n\t}\n}\n\nfunc (s *imageScanner) scan(_ context.Context, img string, sc *Scan) error {\n\tdefer func(t time.Time) {\n\t\ts.log.Debug(\"[Vulscan] perf\",\n\t\t\tslogs.Image, img,\n\t\t\tslogs.Elapsed, time.Since(t),\n\t\t)\n\t}(time.Now())\n\n\tpackages, pkgContext, _, err := pkg.Provide(img, getProviderConfig(s.opts))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to analyze image packages: %w\", err)\n\t}\n\n\tprocessor, err := vex.NewProcessor(vex.ProcessorOptions{\n\t\tDocuments:   s.opts.VexDocuments,\n\t\tIgnoreRules: s.opts.Ignore,\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create VEX processor: %w\", err)\n\t}\n\n\tv := grype.VulnerabilityMatcher{\n\t\tVulnerabilityProvider: s.provider,\n\t\tIgnoreRules:           s.opts.Ignore,\n\t\tNormalizeByCVE:        s.opts.ByCVE,\n\t\tFailSeverity:          s.opts.FailOnSeverity(),\n\t\tMatchers:              getMatchers(s.opts),\n\t\tVexProcessor:          processor,\n\t}\n\n\tvar errs error\n\tmm, _, err := v.FindMatches(packages, pkgContext)\n\tif err != nil {\n\t\terrs = errors.Join(errs, err)\n\t}\n\tif err := sc.run(mm, s.provider); err != nil {\n\t\terrs = errors.Join(errs, err)\n\t}\n\n\treturn errs\n}\n\nfunc getProviderConfig(opts *options.Grype) pkg.ProviderConfig {\n\treturn pkg.ProviderConfig{\n\t\tSyftProviderConfig: pkg.SyftProviderConfig{\n\t\t\tSBOMOptions:            syft.DefaultCreateSBOMConfig(),\n\t\t\tRegistryOptions:        opts.Registry.ToOptions(),\n\t\t\tExclusions:             opts.Exclusions,\n\t\t\tPlatform:               opts.Platform,\n\t\t\tName:                   opts.Name,\n\t\t\tDefaultImagePullSource: opts.DefaultImagePullSource,\n\t\t},\n\t\tSynthesisConfig: pkg.SynthesisConfig{\n\t\t\tGenerateMissingCPEs: opts.GenerateMissingCPEs,\n\t\t},\n\t}\n}\n\nfunc getMatchers(opts *options.Grype) []match.Matcher {\n\treturn matcher.NewDefaultMatchers(\n\t\tmatcher.Config{\n\t\t\tJava: java.MatcherConfig{\n\t\t\t\tExternalSearchConfig: opts.ExternalSources.ToJavaMatcherConfig(),\n\t\t\t\tUseCPEs:              opts.Match.Java.UseCPEs,\n\t\t\t},\n\t\t\tRuby:       ruby.MatcherConfig(opts.Match.Ruby),\n\t\t\tPython:     python.MatcherConfig(opts.Match.Python),\n\t\t\tDotnet:     dotnet.MatcherConfig(opts.Match.Dotnet),\n\t\t\tJavascript: javascript.MatcherConfig(opts.Match.Javascript),\n\t\t\tGolang: golang.MatcherConfig{\n\t\t\t\tUseCPEs:                                opts.Match.Golang.UseCPEs,\n\t\t\t\tAlwaysUseCPEForStdlib:                  opts.Match.Golang.AlwaysUseCPEForStdlib,\n\t\t\t\tAllowMainModulePseudoVersionComparison: opts.Match.Golang.AllowMainModulePseudoVersionComparison,\n\t\t\t},\n\t\t\tStock: stock.MatcherConfig(opts.Match.Stock),\n\t\t},\n\t)\n}\nfunc validateDBLoad(loadErr error, status *vulnerability.ProviderStatus) error {\n\tif loadErr != nil {\n\t\treturn fmt.Errorf(\"failed to load vulnerability db: %w\", loadErr)\n\t}\n\tif status == nil {\n\t\treturn fmt.Errorf(\"unable to determine the status of the vulnerability db\")\n\t}\n\tif status.Error != nil {\n\t\treturn fmt.Errorf(\"db could not be loaded: %w\", status.Error)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/vul/scorer.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage vul\n\nimport \"fmt\"\n\ntype scorer uint8\n\nfunc (b scorer) String() string {\n\treturn fmt.Sprintf(\"%08b\", b)[:6]\n}\n\nfunc newScorer(t tally) scorer {\n\treturn fromTally(t)\n}\n\nfunc (b scorer) Add(b1 scorer) scorer {\n\treturn b | b1\n}\n\nfunc fromTally(t tally) scorer {\n\tvar b scorer\n\tfor i, v := range t {\n\t\tif v == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tswitch i {\n\t\tcase sevCritical:\n\t\t\tb |= 0x80\n\t\tcase sevHigh:\n\t\t\tb |= 0x40\n\t\tcase sevMedium:\n\t\t\tb |= 0x20\n\t\tcase sevLow:\n\t\t\tb |= 0x10\n\t\tcase sevNegligible:\n\t\t\tb |= 0x08\n\t\tcase sevUnknown:\n\t\t\tb |= 0x04\n\t\t}\n\t}\n\n\treturn b\n}\n"
  },
  {
    "path": "internal/vul/scorer_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage vul\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_scorerAdd(t *testing.T) {\n\tuu := map[string]struct {\n\t\tb, b1, e scorer\n\t}{\n\t\t\"zero\": {},\n\t\t\"same\": {\n\t\t\tb:  scorer(0x80),\n\t\t\tb1: scorer(0x80),\n\t\t\te:  scorer(0x80),\n\t\t},\n\t\t\"c+h\": {\n\t\t\tb:  scorer(0x80),\n\t\t\tb1: scorer(0x40),\n\t\t\te:  scorer(0xC0),\n\t\t},\n\t\t\"ch+hm\": {\n\t\t\tb:  scorer(0xc0),\n\t\t\tb1: scorer(0xa0),\n\t\t\te:  scorer(0xe0),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.b.Add(u.b1))\n\t\t})\n\t}\n}\n\nfunc Test_scorerFromTally(t *testing.T) {\n\tuu := map[string]struct {\n\t\ttt tally\n\t\tb  scorer\n\t}{\n\t\t\"zero\": {},\n\t\t\"critical\": {\n\t\t\ttt: tally{29, 0, 0, 0, 0, 0, 0},\n\t\t\tb:  scorer(0x80),\n\t\t},\n\t\t\"high\": {\n\t\t\ttt: tally{0, 17, 0, 0, 0, 0, 0},\n\t\t\tb:  scorer(0x40),\n\t\t},\n\t\t\"medium\": {\n\t\t\ttt: tally{0, 0, 5, 0, 0, 0, 0},\n\t\t\tb:  scorer(0x20),\n\t\t},\n\t\t\"low\": {\n\t\t\ttt: tally{0, 0, 0, 10, 0, 0, 0},\n\t\t\tb:  scorer(0x10),\n\t\t},\n\t\t\"negligible\": {\n\t\t\ttt: tally{0, 0, 0, 0, 10, 0, 0},\n\t\t\tb:  scorer(0x08),\n\t\t},\n\t\t\"unknown\": {\n\t\t\ttt: tally{0, 0, 0, 0, 0, 10, 0},\n\t\t\tb:  scorer(0x04),\n\t\t},\n\t\t\"c/h\": {\n\t\t\ttt: tally{10, 20, 0, 0, 0, 0, 0},\n\t\t\tb:  scorer(0xC0),\n\t\t},\n\t\t\"c/m\": {\n\t\t\ttt: tally{10, 0, 20, 0, 0, 0, 0},\n\t\t\tb:  scorer(0xA0),\n\t\t},\n\t\t\"c/h/l\": {\n\t\t\ttt: tally{10, 1, 20, 0, 0, 0, 0},\n\t\t\tb:  scorer(0xE0),\n\t\t},\n\t\t\"n/u\": {\n\t\t\ttt: tally{0, 0, 0, 0, 10, 20, 0},\n\t\t\tb:  scorer(0x0C),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.b, newScorer(u.tt))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/vul/table.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage vul\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/olekukonko/tablewriter\"\n\t\"github.com/olekukonko/tablewriter/renderer\"\n\t\"github.com/olekukonko/tablewriter/tw\"\n)\n\nconst (\n\tnameIdx = iota\n\tverIdx\n\tfixIdx\n\ttypeIdx\n\tvulIdx\n\tsevIdx\n)\n\ntype Row []string\n\nfunc newRow(ss ...string) Row {\n\tr := make(Row, 0, len(ss))\n\tfor i, s := range ss {\n\t\tif i == sevIdx {\n\t\t\ts = toSev(s)\n\t\t}\n\t\tr = append(r, s)\n\t}\n\treturn r\n}\n\nfunc toSev(s string) string {\n\tswitch s {\n\tcase \"Critical\":\n\t\treturn Sev1\n\tcase \"High\":\n\t\treturn Sev2\n\tcase \"Medium\":\n\t\treturn Sev3\n\tcase \"Low\":\n\t\treturn Sev4\n\tcase \"Negligible\":\n\t\treturn Sev5\n\tdefault:\n\t\treturn SevU\n\t}\n}\n\nfunc (r Row) Name() string          { return r[nameIdx] }\nfunc (r Row) Version() string       { return r[verIdx] }\nfunc (r Row) Fix() string           { return r[fixIdx] }\nfunc (r Row) Type() string          { return r[typeIdx] }\nfunc (r Row) Vulnerability() string { return r[vulIdx] }\nfunc (r Row) Severity() string      { return r[sevIdx] }\n\nfunc sevColor(s string) string {\n\tswitch strings.ToLower(s) {\n\tcase \"critical\":\n\t\treturn fmt.Sprintf(\"[red::b]%s[-::-]\", s)\n\tcase \"high\":\n\t\treturn fmt.Sprintf(\"[orange::b]%s[-::-]\", s)\n\tcase \"medium\":\n\t\treturn fmt.Sprintf(\"[yellow::b]%s[-::-]\", s)\n\tcase \"low\":\n\t\treturn fmt.Sprintf(\"[blue::b]%s[-::-]\", s)\n\tdefault:\n\t\treturn fmt.Sprintf(\"[gray::b]%s[-::-]\", s)\n\t}\n}\n\ntype table struct {\n\tRows []Row\n}\n\nfunc newTable() *table {\n\treturn &table{}\n}\n\nfunc (t *table) dedup() {\n\tvar (\n\t\tseen = make(map[string]struct{}, len(t.Rows))\n\t\trr   = make([]Row, 0, len(t.Rows))\n\t)\n\tfor _, v := range t.Rows {\n\t\tkey := strings.Join(v, \"|\")\n\t\tif _, ok := seen[key]; ok {\n\t\t\tcontinue\n\t\t}\n\t\trr, seen[key] = append(rr, v), struct{}{}\n\t}\n\tt.Rows = rr\n}\n\nfunc (t *table) addRow(r Row) {\n\tt.Rows = append(t.Rows, r)\n}\n\nfunc (t *table) dump(w io.Writer) error {\n\tcolumns := []string{\"Name\", \"Installed\", \"Fixed-In\", \"Type\", \"Vulnerability\", \"Severity\"}\n\n\tascii := tw.NewSymbols(tw.StyleASCII)\n\n\tcfg := tablewriter.Config{\n\t\tBehavior: tw.Behavior{TrimSpace: tw.On},\n\t\tRow: tw.CellConfig{\n\t\t\tPadding: tw.CellPadding{\n\t\t\t\tGlobal: tw.Padding{Left: \"  \", Right: \"  \"}, // 2‑space pad\n\t\t\t},\n\t\t\tAlignment: tw.CellAlignment{Global: tw.AlignLeft},\n\t\t},\n\t}\n\n\ttable := tablewriter.NewTable(\n\t\tw,\n\t\ttablewriter.WithRenderer(renderer.NewBlueprint(tw.Rendition{\n\t\t\tBorders: tw.BorderNone,\n\t\t\tSettings: tw.Settings{\n\t\t\t\tSeparators: tw.SeparatorsNone,\n\t\t\t\tLines:      tw.LinesNone,\n\t\t\t},\n\t\t\tSymbols: ascii,\n\t\t})),\n\t\ttablewriter.WithConfig(cfg),\n\t)\n\n\ttable.Header(columns)\n\n\tfor _, row := range t.Rows {\n\t\terr := table.Append(colorize(row))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn table.Render()\n}\n\nfunc (t *table) sort() {\n\tt.dedup()\n\n\tsort.SliceStable(t.Rows, func(i, j int) bool {\n\t\tif t.Rows[i][nameIdx] != t.Rows[j][nameIdx] {\n\t\t\treturn t.Rows[i][nameIdx] < t.Rows[j][nameIdx]\n\t\t}\n\t\tif t.Rows[i][verIdx] != t.Rows[j][verIdx] {\n\t\t\treturn t.Rows[i][verIdx] < t.Rows[j][verIdx]\n\t\t}\n\t\tif t.Rows[i][typeIdx] != t.Rows[j][typeIdx] {\n\t\t\treturn t.Rows[i][typeIdx] < t.Rows[j][typeIdx]\n\t\t}\n\n\t\tif t.Rows[i][sevIdx] == t.Rows[j][sevIdx] {\n\t\t\treturn t.Rows[i][vulIdx] < t.Rows[j][vulIdx]\n\t\t}\n\t\treturn sevToScore(t.Rows[i][sevIdx]) < sevToScore(t.Rows[j][sevIdx])\n\t})\n}\n\nfunc (t *table) sortSev() {\n\tt.dedup()\n\n\tsort.SliceStable(t.Rows, func(i, j int) bool {\n\t\tif s1, s2 := sevToScore(t.Rows[i][sevIdx]), sevToScore(t.Rows[j][sevIdx]); s1 != s2 {\n\t\t\treturn s1 < s2\n\t\t}\n\t\tif t.Rows[i][nameIdx] != t.Rows[j][nameIdx] {\n\t\t\treturn t.Rows[i][nameIdx] < t.Rows[j][nameIdx]\n\t\t}\n\t\tif t.Rows[i][verIdx] != t.Rows[j][verIdx] {\n\t\t\treturn t.Rows[i][verIdx] < t.Rows[j][verIdx]\n\t\t}\n\t\tif t.Rows[i][typeIdx] != t.Rows[j][typeIdx] {\n\t\t\treturn t.Rows[i][typeIdx] < t.Rows[j][typeIdx]\n\t\t}\n\n\t\treturn t.Rows[i][vulIdx] < t.Rows[j][vulIdx]\n\t})\n}\n\nfunc sevToScore(s string) int {\n\tswitch s {\n\tcase Sev1:\n\t\treturn 1\n\tcase Sev2:\n\t\treturn 2\n\tcase Sev3:\n\t\treturn 3\n\tcase Sev4:\n\t\treturn 4\n\tcase Sev5:\n\t\treturn 5\n\tdefault:\n\t\treturn 6\n\t}\n}\n"
  },
  {
    "path": "internal/vul/table_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage vul\n\nimport (\n\t\"bufio\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc Test_sort(t *testing.T) {\n\tuu := map[string]struct {\n\t\tt1, t2 *table\n\t}{\n\t\t\"simple\": {\n\t\t\tt1: makeTable(t, \"testdata/sort/no_dups/sc1.text\"),\n\t\t\tt2: makeTable(t, \"testdata/sort/no_dups/sc2.text\"),\n\t\t},\n\t\t\"dups\": {\n\t\t\tt1: makeTable(t, \"testdata/sort/dups/sc1.text\"),\n\t\t\tt2: makeTable(t, \"testdata/sort/dups/sc2.text\"),\n\t\t},\n\t\t\"full\": {\n\t\t\tt1: makeTable(t, \"testdata/sort/full/sc1.text\"),\n\t\t\tt2: makeTable(t, \"testdata/sort/full/sc2.text\"),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tu.t1.sort()\n\t\t\tassert.Equal(t, u.t2, u.t1)\n\t\t})\n\t}\n}\n\nfunc Test_sortSev(t *testing.T) {\n\tuu := map[string]struct {\n\t\tt1, t2 *table\n\t}{\n\t\t\"simple\": {\n\t\t\tt1: makeTable(t, \"testdata/sort_sev/no_dups/sc1.text\"),\n\t\t\tt2: makeTable(t, \"testdata/sort_sev/no_dups/sc2.text\"),\n\t\t},\n\t\t\"dups\": {\n\t\t\tt1: makeTable(t, \"testdata/sort_sev/dups/sc1.text\"),\n\t\t\tt2: makeTable(t, \"testdata/sort_sev/dups/sc2.text\"),\n\t\t},\n\t\t\"full\": {\n\t\t\tt1: makeTable(t, \"testdata/sort_sev/full/sc1.text\"),\n\t\t\tt2: makeTable(t, \"testdata/sort_sev/full/sc2.text\"),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tu.t1.sortSev()\n\t\t\tassert.Equal(t, u.t2, u.t1)\n\t\t})\n\t}\n}\n\n// Helpers...\n\nfunc makeTable(t *testing.T, path string) *table {\n\tf, err := os.Open(path)\n\tdefer func() {\n\t\t_ = f.Close()\n\t}()\n\trequire.NoError(t, err)\n\tsc := bufio.NewScanner(f)\n\tvar tt table\n\tfor sc.Scan() {\n\t\tff := strings.Fields(sc.Text())\n\t\ttt.addRow(newRow(ff...))\n\t}\n\trequire.NoError(t, sc.Err())\n\n\treturn &tt\n}\n"
  },
  {
    "path": "internal/vul/tally.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage vul\n\nimport (\n\t\"fmt\"\n\t\"io\"\n)\n\nconst (\n\tsevCritical = iota\n\tsevHigh\n\tsevMedium\n\tsevLow\n\tsevNegligible\n\tsevUnknown\n\tsevFixed\n)\n\nvar vulWeights = []int{10_000, 100, 100, 10, 0, 0, 0, 0}\n\ntype tally [7]int\n\nfunc newTally(t *table) tally {\n\tvar tt tally\n\tfor _, r := range t.Rows {\n\t\tif r.Fix() != \"\" {\n\t\t\ttt[sevFixed]++\n\t\t}\n\t\tswitch r.Severity() {\n\t\tcase Sev1:\n\t\t\ttt[sevCritical]++\n\t\tcase Sev2:\n\t\t\ttt[sevHigh]++\n\t\tcase Sev3:\n\t\t\ttt[sevMedium]++\n\t\tcase Sev4:\n\t\t\ttt[sevLow]++\n\t\tcase Sev5:\n\t\t\ttt[sevNegligible]++\n\t\tcase SevU:\n\t\t\ttt[sevUnknown]++\n\t\t}\n\t}\n\n\treturn tt\n}\n\n// Dump dumps tally as text.\nfunc (t tally) Dump(w io.Writer) {\n\t_, _ = fmt.Fprintf(w, \"%d critical, %d high, %d medium, %d low, %d negligible\",\n\t\tt[sevCritical],\n\t\tt[sevHigh],\n\t\tt[sevMedium],\n\t\tt[sevLow],\n\t\tt[sevNegligible],\n\t)\n\tif t[sevUnknown] > 0 {\n\t\t_, _ = fmt.Fprintf(w, \" (%d unknown)\", t[sevUnknown])\n\t}\n\tif t[sevFixed] > 0 {\n\t\t_, _ = fmt.Fprintf(w, \" -- [Fixed: %d]\", t[sevFixed])\n\t}\n}\n\nfunc (t *tally) score() int {\n\tvar s int\n\tfor i, v := range t[:5] {\n\t\ts += v * vulWeights[i]\n\t}\n\n\treturn s\n}\n"
  },
  {
    "path": "internal/vul/tally_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage vul\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc Test_newTally(t *testing.T) {\n\tuu := map[string]struct {\n\t\tt  *table\n\t\ttt tally\n\t}{\n\t\t\"full\": {\n\t\t\tt:  makeTable(t, \"testdata/sort/full/sc2.text\"),\n\t\t\ttt: tally{7, 14, 8, 0, 0, 0, 29},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.tt, newTally(u.t))\n\t\t})\n\t}\n}\n\nfunc Test_score(t *testing.T) {\n\tuu := map[string]struct {\n\t\ttt tally\n\t\tsc int\n\t}{\n\t\t\"zero\": {},\n\t\t\"critical\": {\n\t\t\ttt: tally{29, 7, 14, 8, 0, 0, 0},\n\t\t\tsc: 292180,\n\t\t},\n\t\t\"high\": {\n\t\t\ttt: tally{0, 17, 14, 8, 0, 0, 0},\n\t\t\tsc: 3180,\n\t\t},\n\t\t\"medium\": {\n\t\t\ttt: tally{0, 0, 14, 0, 0, 0, 0},\n\t\t\tsc: 1400,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.sc, u.tt.score())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/vul/testdata/sort/dups/sc1.text",
    "content": "busybox                             1.34.1     n/a       binary     CVE-2022-48174       Critical\nbusybox                             1.34.1     n/a       binary     CVE-2022-28391       High\ngolang.org/x/net                    v0.4.0     0.17.0    go-module  GHSA-4374-p667-p6c8  High\ngolang.org/x/net                    v0.4.0     0.7.0     go-module  GHSA-vvpx-j8f3-3w6h  High\ngolang.org/x/net                    v0.4.0     0.7.0     go-module  GHSA-vvpx-j8f3-3w6h  High\ngolang.org/x/net                    v0.4.0     0.17.0    go-module  GHSA-qppj-fm5r-hxr3  Medium\ngolang.org/x/net                    v0.4.0     0.13.0    go-module  GHSA-2wrh-6pvc-2jm9  Medium\ngolang.org/x/net                    v0.4.0     0.13.0    go-module  GHSA-2wrh-6pvc-2jm9  Medium\ngolang.org/x/net                    v0.4.0     0.17.0    go-module  GHSA-qppj-fm5r-hxr3  Medium"
  },
  {
    "path": "internal/vul/testdata/sort/dups/sc2.text",
    "content": "busybox                             1.34.1     n/a       binary     CVE-2022-48174       Critical\nbusybox                             1.34.1     n/a       binary     CVE-2022-28391       High\ngolang.org/x/net                    v0.4.0     0.17.0    go-module  GHSA-4374-p667-p6c8  High\ngolang.org/x/net                    v0.4.0     0.7.0     go-module  GHSA-vvpx-j8f3-3w6h  High\ngolang.org/x/net                    v0.4.0     0.13.0    go-module  GHSA-2wrh-6pvc-2jm9  Medium\ngolang.org/x/net                    v0.4.0     0.17.0    go-module  GHSA-qppj-fm5r-hxr3  Medium"
  },
  {
    "path": "internal/vul/testdata/sort/full/sc1.text",
    "content": "busybox                             1.34.1     n/a          binary     CVE-2022-48174       Critical\nbusybox                             1.34.1     n/a          binary     CVE-2022-28391       High\ngithub.com/prometheus/alertmanager  v0.25.0    0.25.1    go-module  GHSA-v86x-5fm3-5p7j  Medium\ngithub.com/prometheus/alertmanager  v0.25.0    0.25.1    go-module  GHSA-v86x-5fm3-5p7j  Medium\ngolang.org/x/net                    v0.4.0     0.7.0     go-module  GHSA-vvpx-j8f3-3w6h  High\ngolang.org/x/net                    v0.4.0     0.7.0     go-module  GHSA-vvpx-j8f3-3w6h  High\ngolang.org/x/net                    v0.4.0     0.17.0    go-module  GHSA-4374-p667-p6c8  High\ngolang.org/x/net                    v0.4.0     0.17.0    go-module  GHSA-4374-p667-p6c8  High\ngolang.org/x/net                    v0.4.0     0.17.0    go-module  GHSA-qppj-fm5r-hxr3  Medium\ngolang.org/x/net                    v0.4.0     0.17.0    go-module  GHSA-qppj-fm5r-hxr3  Medium\ngolang.org/x/net                    v0.4.0     0.13.0    go-module  GHSA-2wrh-6pvc-2jm9  Medium\ngolang.org/x/net                    v0.4.0     0.13.0    go-module  GHSA-2wrh-6pvc-2jm9  Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29404       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-39323       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24538       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29405       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24540       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29405       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24540       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24538       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29402       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29402       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29404       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-39323       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41724       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41725       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24534       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29400       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24539       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29403       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-44487       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41722       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41724       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41723       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24534       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41725       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24536       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24537       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24537       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41723       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24536       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29403       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29400       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41722       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24539       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-44487       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29406       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29409       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29409       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24532       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-39319       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24532       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29406       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-39318       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-39319       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-39318       Medium"
  },
  {
    "path": "internal/vul/testdata/sort/full/sc2.text",
    "content": "busybox                             1.34.1     n/a          binary     CVE-2022-48174       Critical\nbusybox                             1.34.1     n/a          binary     CVE-2022-28391       High\ngithub.com/prometheus/alertmanager  v0.25.0    0.25.1       go-module  GHSA-v86x-5fm3-5p7j  Medium\ngolang.org/x/net                    v0.4.0     0.17.0       go-module  GHSA-4374-p667-p6c8  High\ngolang.org/x/net                    v0.4.0     0.7.0        go-module  GHSA-vvpx-j8f3-3w6h  High\ngolang.org/x/net                    v0.4.0     0.13.0       go-module  GHSA-2wrh-6pvc-2jm9  Medium\ngolang.org/x/net                    v0.4.0     0.17.0       go-module  GHSA-qppj-fm5r-hxr3  Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24538       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24540       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29402       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29404       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29405       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-39323       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41722       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41723       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41724       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41725       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24534       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24536       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24537       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24539       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29400       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29403       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-44487       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24532       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29406       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29409       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-39318       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-39319       Medium"
  },
  {
    "path": "internal/vul/testdata/sort/no_dups/sc1.text",
    "content": "busybox                             1.34.1       n/a        binary     CVE-2022-48174       Critical\nbusybox                             1.34.1       n/a        binary     CVE-2022-28391       High"
  },
  {
    "path": "internal/vul/testdata/sort/no_dups/sc2.text",
    "content": "busybox                             1.34.1      n/a         binary     CVE-2022-48174       Critical\nbusybox                             1.34.1      n/a         binary     CVE-2022-28391       High"
  },
  {
    "path": "internal/vul/testdata/sort_sev/dups/sc1.text",
    "content": "busybox                             1.34.1     n/a       binary     CVE-2022-48174       Critical\nbusybox                             1.34.1     n/a       binary     CVE-2022-28391       High\ngolang.org/x/net                    v0.4.0     0.17.0    go-module  GHSA-4374-p667-p6c8  High\ngolang.org/x/net                    v0.4.0     0.7.0     go-module  GHSA-vvpx-j8f3-3w6h  High\ngolang.org/x/net                    v0.4.0     0.7.0     go-module  GHSA-vvpx-j8f3-3w6h  High\ngolang.org/x/net                    v0.4.0     0.17.0    go-module  GHSA-qppj-fm5r-hxr3  Medium\ngolang.org/x/net                    v0.4.0     0.13.0    go-module  GHSA-2wrh-6pvc-2jm9  Medium\ngolang.org/x/net                    v0.4.0     0.13.0    go-module  GHSA-2wrh-6pvc-2jm9  Medium\ngolang.org/x/net                    v0.4.0     0.17.0    go-module  GHSA-qppj-fm5r-hxr3  Medium"
  },
  {
    "path": "internal/vul/testdata/sort_sev/dups/sc2.text",
    "content": "busybox                             1.34.1     n/a       binary     CVE-2022-48174       Critical\nbusybox                             1.34.1     n/a       binary     CVE-2022-28391       High\ngolang.org/x/net                    v0.4.0     0.17.0    go-module  GHSA-4374-p667-p6c8  High\ngolang.org/x/net                    v0.4.0     0.7.0     go-module  GHSA-vvpx-j8f3-3w6h  High\ngolang.org/x/net                    v0.4.0     0.13.0    go-module  GHSA-2wrh-6pvc-2jm9  Medium\ngolang.org/x/net                    v0.4.0     0.17.0    go-module  GHSA-qppj-fm5r-hxr3  Medium"
  },
  {
    "path": "internal/vul/testdata/sort_sev/full/sc1.text",
    "content": "busybox                             1.34.1     n/a          binary     CVE-2022-48174       Critical\nbusybox                             1.34.1     n/a          binary     CVE-2022-28391       High\ngithub.com/prometheus/alertmanager  v0.25.0    0.25.1    go-module  GHSA-v86x-5fm3-5p7j  Medium\ngithub.com/prometheus/alertmanager  v0.25.0    0.25.1    go-module  GHSA-v86x-5fm3-5p7j  Medium\ngolang.org/x/net                    v0.4.0     0.7.0     go-module  GHSA-vvpx-j8f3-3w6h  High\ngolang.org/x/net                    v0.4.0     0.7.0     go-module  GHSA-vvpx-j8f3-3w6h  High\ngolang.org/x/net                    v0.4.0     0.17.0    go-module  GHSA-4374-p667-p6c8  High\ngolang.org/x/net                    v0.4.0     0.17.0    go-module  GHSA-4374-p667-p6c8  High\ngolang.org/x/net                    v0.4.0     0.17.0    go-module  GHSA-qppj-fm5r-hxr3  Medium\ngolang.org/x/net                    v0.4.0     0.17.0    go-module  GHSA-qppj-fm5r-hxr3  Medium\ngolang.org/x/net                    v0.4.0     0.13.0    go-module  GHSA-2wrh-6pvc-2jm9  Medium\ngolang.org/x/net                    v0.4.0     0.13.0    go-module  GHSA-2wrh-6pvc-2jm9  Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29404       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-39323       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24538       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29405       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24540       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29405       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24540       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24538       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29402       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29402       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29404       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-39323       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41724       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41725       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24534       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29400       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24539       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29403       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-44487       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41722       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41724       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41723       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24534       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41725       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24536       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24537       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24537       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41723       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24536       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29403       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29400       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41722       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24539       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-44487       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29406       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29409       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29409       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24532       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-39319       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24532       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29406       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-39318       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-39319       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-39318       Medium"
  },
  {
    "path": "internal/vul/testdata/sort_sev/full/sc2.text",
    "content": "busybox                             1.34.1     n/a          binary     CVE-2022-48174       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24538       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24540       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29402       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29404       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29405       Critical\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-39323       Critical\nbusybox                             1.34.1     n/a          binary     CVE-2022-28391       High\ngolang.org/x/net                    v0.4.0     0.17.0       go-module  GHSA-4374-p667-p6c8  High\ngolang.org/x/net                    v0.4.0     0.7.0        go-module  GHSA-vvpx-j8f3-3w6h  High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41722       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41723       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41724       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2022-41725       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24534       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24536       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24537       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24539       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29400       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29403       High\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-44487       High\ngithub.com/prometheus/alertmanager  v0.25.0    0.25.1       go-module  GHSA-v86x-5fm3-5p7j  Medium\ngolang.org/x/net                    v0.4.0     0.13.0       go-module  GHSA-2wrh-6pvc-2jm9  Medium\ngolang.org/x/net                    v0.4.0     0.17.0       go-module  GHSA-qppj-fm5r-hxr3  Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-24532       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29406       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-29409       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-39318       Medium\nstdlib                              go1.19.4   n/a          go-module  CVE-2023-39319       Medium"
  },
  {
    "path": "internal/vul/testdata/sort_sev/no_dups/sc1.text",
    "content": "busybox                             1.34.1       n/a        binary     CVE-2022-48174       Critical\nbusybox                             1.34.1       n/a        binary     CVE-2022-28391       High"
  },
  {
    "path": "internal/vul/testdata/sort_sev/no_dups/sc2.text",
    "content": "busybox                             1.34.1      n/a         binary     CVE-2022-48174       Critical\nbusybox                             1.34.1      n/a         binary     CVE-2022-28391       High"
  },
  {
    "path": "internal/vul/types.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage vul\n\nconst (\n\t// Sev1 tracks Critical sev.\n\tSev1 = \"SEV-1\"\n\n\t// Sev2 tracks High sev.\n\tSev2 = \"SEV-2\"\n\n\t// Sev3 tracks Medium sev.\n\tSev3 = \"SEV-3\"\n\n\t// Sev4 tracks Low sev.\n\tSev4 = \"SEV-4\"\n\n\t// Sev5 tracks Negligible sev.\n\tSev5 = \"SEV-5\"\n\n\t// SevU tracks Unknown sev.\n\tSevU = \"SEV-U\"\n)\n"
  },
  {
    "path": "internal/watch/factory.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage watch\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\tdi \"k8s.io/client-go/dynamic/dynamicinformer\"\n\t\"k8s.io/client-go/informers\"\n)\n\nconst (\n\tdefaultResync   = 10 * time.Minute\n\tdefaultWaitTime = 500 * time.Millisecond\n)\n\n// Factory tracks various resource informers.\ntype Factory struct {\n\tfactories  map[string]di.DynamicSharedInformerFactory\n\tclient     client.Connection\n\tstopChan   chan struct{}\n\tforwarders Forwarders\n\tmx         sync.RWMutex\n}\n\n// NewFactory returns a new informers factory.\nfunc NewFactory(clt client.Connection) *Factory {\n\treturn &Factory{\n\t\tclient:     clt,\n\t\tfactories:  make(map[string]di.DynamicSharedInformerFactory),\n\t\tforwarders: NewForwarders(),\n\t}\n}\n\n// Start initializes the informers until caller cancels the context.\nfunc (f *Factory) Start(ns string) {\n\tf.mx.Lock()\n\tdefer f.mx.Unlock()\n\n\tslog.Debug(\"Factory started\", slogs.Namespace, ns)\n\tf.stopChan = make(chan struct{})\n\tfor ns, fac := range f.factories {\n\t\tslog.Debug(\"Starting factory for ns\", slogs.Namespace, ns)\n\t\tfac.Start(f.stopChan)\n\t}\n}\n\n// Terminate terminates all watchers and forwards.\nfunc (f *Factory) Terminate() {\n\tf.mx.Lock()\n\tdefer f.mx.Unlock()\n\n\tif f.stopChan != nil {\n\t\tclose(f.stopChan)\n\t\tf.stopChan = nil\n\t}\n\tfor k := range f.factories {\n\t\tdelete(f.factories, k)\n\t}\n\tf.forwarders.DeleteAll()\n}\n\n// List returns a resource collection.\nfunc (f *Factory) List(gvr *client.GVR, ns string, wait bool, lbls labels.Selector) ([]runtime.Object, error) {\n\tif client.IsAllNamespace(ns) {\n\t\tns = client.BlankNamespace\n\t}\n\tinf, err := f.CanForResource(ns, gvr, client.ListAccess)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar oo []runtime.Object\n\tif client.IsClusterScoped(ns) {\n\t\too, err = inf.Lister().List(lbls)\n\t} else {\n\t\too, err = inf.Lister().ByNamespace(ns).List(lbls)\n\t}\n\tif !wait || (wait && inf.Informer().HasSynced()) {\n\t\treturn oo, err\n\t}\n\n\tf.waitForCacheSync(ns)\n\tif client.IsClusterScoped(ns) {\n\t\treturn inf.Lister().List(lbls)\n\t}\n\treturn inf.Lister().ByNamespace(ns).List(lbls)\n}\n\n// HasSynced checks if given informer is up to date.\nfunc (f *Factory) HasSynced(gvr *client.GVR, ns string) (bool, error) {\n\tinf, err := f.CanForResource(ns, gvr, client.ListAccess)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\treturn inf.Informer().HasSynced(), nil\n}\n\n// Get retrieves a given resource.\nfunc (f *Factory) Get(gvr *client.GVR, fqn string, wait bool, _ labels.Selector) (runtime.Object, error) {\n\tns, n := namespaced(fqn)\n\tif client.IsAllNamespace(ns) {\n\t\tns = client.BlankNamespace\n\t}\n\n\tinf, err := f.CanForInstance(fqn, gvr, []string{client.GetVerb})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar o runtime.Object\n\tif client.IsClusterScoped(ns) {\n\t\to, err = inf.Lister().Get(n)\n\t} else {\n\t\to, err = inf.Lister().ByNamespace(ns).Get(n)\n\t}\n\tif !wait || (wait && inf.Informer().HasSynced()) {\n\t\treturn o, err\n\t}\n\n\tf.waitForCacheSync(ns)\n\tif client.IsClusterScoped(ns) {\n\t\treturn inf.Lister().Get(n)\n\t}\n\n\treturn inf.Lister().ByNamespace(ns).Get(n)\n}\n\nfunc (f *Factory) waitForCacheSync(ns string) {\n\tif client.IsClusterWide(ns) {\n\t\tns = client.BlankNamespace\n\t}\n\n\tf.mx.RLock()\n\tdefer f.mx.RUnlock()\n\tfac, ok := f.factories[ns]\n\tif !ok {\n\t\treturn\n\t}\n\n\t// Hang for a sec for the cache to refresh if still not done bail out!\n\tc := make(chan struct{})\n\tgo func(c chan struct{}) {\n\t\t<-time.After(defaultWaitTime)\n\t\tclose(c)\n\t}(c)\n\t_ = fac.WaitForCacheSync(c)\n}\n\n// WaitForCacheSync waits for all factories to update their cache.\nfunc (f *Factory) WaitForCacheSync() {\n\tfor ns, fac := range f.factories {\n\t\tm := fac.WaitForCacheSync(f.stopChan)\n\t\tfor k, v := range m {\n\t\t\tslog.Debug(\"CACHE `%q Loaded %t:%s\",\n\t\t\t\tslogs.Namespace, ns,\n\t\t\t\tslogs.ResGrpVersion, v,\n\t\t\t\tslogs.ResKind, k,\n\t\t\t)\n\t\t}\n\t}\n}\n\n// Client return the factory connection.\nfunc (f *Factory) Client() client.Connection {\n\treturn f.client\n}\n\n// FactoryFor returns a factory for a given namespace.\nfunc (f *Factory) FactoryFor(ns string) di.DynamicSharedInformerFactory {\n\treturn f.factories[ns]\n}\n\n// SetActiveNS sets the active namespace.\nfunc (f *Factory) SetActiveNS(ns string) error {\n\tif f.isClusterWide() {\n\t\treturn nil\n\t}\n\t_, err := f.ensureFactory(ns)\n\treturn err\n}\n\nfunc (f *Factory) isClusterWide() bool {\n\tf.mx.RLock()\n\tdefer f.mx.RUnlock()\n\t_, ok := f.factories[client.BlankNamespace]\n\n\treturn ok\n}\n\n// CanForResource return an informer is user has access.\nfunc (f *Factory) CanForResource(ns string, gvr *client.GVR, verbs []string) (informers.GenericInformer, error) {\n\tvar resName string\n\tif gvr == client.NsGVR {\n\t\tresName = ns\n\t}\n\tauth, err := f.Client().CanI(ns, gvr, resName, verbs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !auth {\n\t\treturn nil, fmt.Errorf(\"%v access denied on resource %q:%q\", verbs, ns, gvr)\n\t}\n\n\treturn f.ForResource(ns, gvr)\n}\n\n// CanForInstance return an informer is user has access.\nfunc (f *Factory) CanForInstance(fqn string, gvr *client.GVR, verbs []string) (informers.GenericInformer, error) {\n\tns, n := namespaced(fqn)\n\tif client.IsAllNamespace(ns) {\n\t\tns = client.BlankNamespace\n\t}\n\n\t// For namespace resources, set namespace to the resource name to allow\n\t// RoleBindings within that namespace to grant permissions\n\tif gvr == client.NsGVR {\n\t\tns = n\n\t}\n\n\tauth, err := f.Client().CanI(ns, gvr, n, verbs)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif !auth {\n\t\treturn nil, fmt.Errorf(\"%v access denied on resource %q:%q\", verbs, ns, gvr)\n\t}\n\n\treturn f.ForResource(ns, gvr)\n}\n\n// ForResource returns an informer for a given resource.\nfunc (f *Factory) ForResource(ns string, gvr *client.GVR) (informers.GenericInformer, error) {\n\tfact, err := f.ensureFactory(ns)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tinf := fact.ForResource(gvr.GVR())\n\tif inf == nil {\n\t\tslog.Error(\"No informer found\",\n\t\t\tslogs.GVR, gvr,\n\t\t\tslogs.Namespace, ns,\n\t\t)\n\t\treturn inf, nil\n\t}\n\n\tf.mx.RLock()\n\tdefer f.mx.RUnlock()\n\tfact.Start(f.stopChan)\n\n\treturn inf, nil\n}\n\nfunc (f *Factory) ensureFactory(ns string) (di.DynamicSharedInformerFactory, error) {\n\tif client.IsClusterWide(ns) {\n\t\tns = client.BlankNamespace\n\t}\n\tf.mx.Lock()\n\tdefer f.mx.Unlock()\n\tif fac, ok := f.factories[ns]; ok {\n\t\treturn fac, nil\n\t}\n\n\tdial, err := f.client.DynDial()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tf.factories[ns] = di.NewFilteredDynamicSharedInformerFactory(\n\t\tdial,\n\t\tdefaultResync,\n\t\tns,\n\t\tnil,\n\t)\n\n\treturn f.factories[ns], nil\n}\n\n// AddForwarder registers a new portforward for a given container.\nfunc (f *Factory) AddForwarder(pf Forwarder) {\n\tf.mx.Lock()\n\tdefer f.mx.Unlock()\n\n\tf.forwarders[pf.ID()] = pf\n}\n\n// DeleteForwarder deletes portforward for a given container.\nfunc (f *Factory) DeleteForwarder(path string) {\n\tcount := f.forwarders.Kill(path)\n\tslog.Warn(\"Deleted portforward\",\n\t\tslogs.Count, count,\n\t\tslogs.GVR, path,\n\t)\n}\n\n// Forwarders returns all portforwards.\nfunc (f *Factory) Forwarders() Forwarders {\n\tf.mx.RLock()\n\tdefer f.mx.RUnlock()\n\n\treturn f.forwarders\n}\n\n// ForwarderFor returns a portforward for a given container or nil if none exists.\nfunc (f *Factory) ForwarderFor(path string) (Forwarder, bool) {\n\tf.mx.RLock()\n\tdefer f.mx.RUnlock()\n\n\tfwd, ok := f.forwarders[path]\n\n\treturn fwd, ok\n}\n\n// ValidatePortForwards check if pods are still around for portforwards.\n// BOZO!! Review!!!\nfunc (f *Factory) ValidatePortForwards() {\n\tfor k, fwd := range f.forwarders {\n\t\ttokens := strings.Split(k, \":\")\n\t\tif len(tokens) != 2 {\n\t\t\tslog.Error(\"Invalid port-forward key\", slogs.Key, k)\n\t\t\treturn\n\t\t}\n\t\tpaths := strings.Split(tokens[0], \"|\")\n\t\tif len(paths) < 1 {\n\t\t\tslog.Error(\"Invalid port-forward path\", slogs.Path, tokens[0])\n\t\t}\n\t\to, err := f.Get(client.PodGVR, paths[0], false, labels.Everything())\n\t\tif err != nil {\n\t\t\tfwd.Stop()\n\t\t\tdelete(f.forwarders, k)\n\t\t\tcontinue\n\t\t}\n\t\tvar pod v1.Pod\n\t\tif err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod); err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif pod.GetCreationTimestamp().Unix() > fwd.Age().Unix() {\n\t\t\tfwd.Stop()\n\t\t\tdelete(f.forwarders, k)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/watch/forwarders.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage watch\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/port\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\t\"k8s.io/client-go/tools/portforward\"\n)\n\n// Forwarder represents a port forwarder.\ntype Forwarder interface {\n\t// Start starts a port-forward.\n\tStart(path string, tunnel port.PortTunnel) (*portforward.PortForwarder, error)\n\n\t// Stop terminates a port forward.\n\tStop()\n\n\t// ID returns the pf id.\n\tID() string\n\n\t// Container returns a container name.\n\tContainer() string\n\n\t// Port returns the port mapping.\n\tPort() string\n\n\t// Address returns the host address.\n\tAddress() string\n\n\t// FQN returns the full port-forward name.\n\tFQN() string\n\n\t// Active returns forwarder current state.\n\tActive() bool\n\n\t// SetActive sets port-forward state.\n\tSetActive(bool)\n\n\t// Age returns forwarder age.\n\tAge() time.Time\n\n\t// HasPortMapping returns true if port mapping exists.\n\tHasPortMapping(string) bool\n}\n\n// Forwarders tracks active port forwards.\ntype Forwarders map[string]Forwarder\n\n// NewForwarders returns new forwarders.\nfunc NewForwarders() Forwarders {\n\treturn make(map[string]Forwarder)\n}\n\n// IsPodForwarded checks if pod has a forward.\nfunc (ff Forwarders) IsPodForwarded(fqn string) bool {\n\tfqn += \"|\"\n\tfor k := range ff {\n\t\tif strings.HasPrefix(k, fqn) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// IsContainerForwarded checks if pod has a forward.\nfunc (ff Forwarders) IsContainerForwarded(fqn, co string) bool {\n\tfqn += \"|\" + co\n\tfor k := range ff {\n\t\tif strings.HasPrefix(k, fqn) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// DeleteAll stops and delete all port-forwards.\nfunc (ff Forwarders) DeleteAll() {\n\tfor k, f := range ff {\n\t\tslog.Debug(\"Deleting forwarder\", slogs.ID, f.ID())\n\t\tf.Stop()\n\t\tdelete(ff, k)\n\t}\n}\n\n// Kill stops and delete a port-forwards associated with pod.\nfunc (ff Forwarders) Kill(path string) int {\n\tvar stats int\n\n\t// The way port forwards are stored is `pod_fqn|container|local_port:container_port`\n\t// The '|' is added to make sure we do not delete port forwards from other pods that have the same prefix\n\t// Without the `|` port forwards for pods, default/web-0 and default/web-0-bla would be both deleted\n\t// even if we want only port forwards for default/web-0 to be deleted\n\tprefix := path + \"|\"\n\tfor k, f := range ff {\n\t\tif k == path || strings.HasPrefix(k, prefix) {\n\t\t\tstats++\n\t\t\tslog.Debug(\"Stop and delete port-forward\", slogs.Name, k)\n\t\t\tf.Stop()\n\t\t\tdelete(ff, k)\n\t\t}\n\t}\n\n\treturn stats\n}\n\n// Dump for debug!\nfunc (ff Forwarders) Dump() {\n\tslog.Debug(\"----------- PORT-FORWARDS --------------\")\n\tfor k, f := range ff {\n\t\tslog.Debug(fmt.Sprintf(\"  %s -- %s\", k, f))\n\t}\n}\n"
  },
  {
    "path": "internal/watch/forwarders_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage watch_test\n\nimport (\n\t\"log/slog\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/derailed/k9s/internal/port\"\n\t\"github.com/derailed/k9s/internal/watch\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"k8s.io/client-go/tools/portforward\"\n)\n\nfunc init() {\n\tslog.SetDefault(slog.New(slog.DiscardHandler))\n}\n\nfunc TestIsPodForwarded(t *testing.T) {\n\tuu := map[string]struct {\n\t\tff  watch.Forwarders\n\t\tfqn string\n\t\te   bool\n\t}{\n\t\t\"happy\": {\n\t\t\tff: watch.Forwarders{\n\t\t\t\t\"ns1/p1||8080:8080\": newNoOpForwarder(),\n\t\t\t},\n\t\t\tfqn: \"ns1/p1\",\n\t\t\te:   true,\n\t\t},\n\t\t\"dud\": {\n\t\t\tff: watch.Forwarders{\n\t\t\t\t\"ns1/p1||8080:8080\": newNoOpForwarder(),\n\t\t\t},\n\t\t\tfqn: \"ns1/p2\",\n\t\t},\n\t\t\"sub\": {\n\t\t\tff: watch.Forwarders{\n\t\t\t\t\"ns1/freddy||8080:8080\": newNoOpForwarder(),\n\t\t\t},\n\t\t\tfqn: \"ns1/fred\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.ff.IsPodForwarded(u.fqn))\n\t\t})\n\t}\n}\n\nfunc TestIsContainerForwarded(t *testing.T) {\n\tuu := map[string]struct {\n\t\tff      watch.Forwarders\n\t\tfqn, co string\n\t\te       bool\n\t}{\n\t\t\"happy\": {\n\t\t\tff: watch.Forwarders{\n\t\t\t\t\"ns1/p1|c1|8080:8080\": newNoOpForwarder(),\n\t\t\t},\n\t\t\tfqn: \"ns1/p1\",\n\t\t\tco:  \"c1\",\n\t\t\te:   true,\n\t\t},\n\t\t\"dud\": {\n\t\t\tff: watch.Forwarders{\n\t\t\t\t\"ns1/p1|c1|8080:8080\": newNoOpForwarder(),\n\t\t\t},\n\t\t\tfqn: \"ns1/p1\",\n\t\t\tco:  \"c2\",\n\t\t},\n\t\t\"sub\": {\n\t\t\tff: watch.Forwarders{\n\t\t\t\t\"ns1/freddy|c1|8080:8080\": newNoOpForwarder(),\n\t\t\t},\n\t\t\tfqn: \"ns1/fred\",\n\t\t\tco:  \"c1\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.ff.IsContainerForwarded(u.fqn, u.co))\n\t\t})\n\t}\n}\n\nfunc TestKill(t *testing.T) {\n\tuu := map[string]struct {\n\t\tff    watch.Forwarders\n\t\tpath  string\n\t\tkills int\n\t}{\n\t\t\"partial_match\": {\n\t\t\tff: watch.Forwarders{\n\t\t\t\t\"ns1/p1|c1|8080:8080\":   newNoOpForwarder(),\n\t\t\t\t\"ns1/p1_1|c1|8080:8080\": newNoOpForwarder(),\n\t\t\t\t\"ns1/p2|c1|8080:8080\":   newNoOpForwarder(),\n\t\t\t},\n\t\t\tpath:  \"ns1/p1\",\n\t\t\tkills: 1,\n\t\t},\n\t\t\"partial_no_match\": {\n\t\t\tff: watch.Forwarders{\n\t\t\t\t\"ns1/p1|c1|8080:8080\":   newNoOpForwarder(),\n\t\t\t\t\"ns1/p1_1|c1|8080:8080\": newNoOpForwarder(),\n\t\t\t\t\"ns1/p2|c1|8080:8080\":   newNoOpForwarder(),\n\t\t\t},\n\t\t\tpath: \"ns1/p\",\n\t\t},\n\t\t\"path_sub\": {\n\t\t\tff: watch.Forwarders{\n\t\t\t\t\"ns1/p1|c1|8080:8080\":   newNoOpForwarder(),\n\t\t\t\t\"ns1/p1_1|c1|8080:8080\": newNoOpForwarder(),\n\t\t\t\t\"ns1/p2|c1|8080:8080\":   newNoOpForwarder(),\n\t\t\t},\n\t\t\tpath:  \"ns1/p1\",\n\t\t\tkills: 1,\n\t\t},\n\t\t\"partial_multi\": {\n\t\t\tff: watch.Forwarders{\n\t\t\t\t\"ns1/p1|c1|8080:8080\": newNoOpForwarder(),\n\t\t\t\t\"ns1/p1|c2|8081:8081\": newNoOpForwarder(),\n\t\t\t\t\"ns1/p2|c1|8080:8080\": newNoOpForwarder(),\n\t\t\t},\n\t\t\tpath:  \"ns1/p1\",\n\t\t\tkills: 2,\n\t\t},\n\t\t\"full_match\": {\n\t\t\tff: watch.Forwarders{\n\t\t\t\t\"ns1/p1|c1|8080:8080\":   newNoOpForwarder(),\n\t\t\t\t\"ns1/p1_1|c1|8080:8080\": newNoOpForwarder(),\n\t\t\t\t\"ns1/p2|c1|8080:8080\":   newNoOpForwarder(),\n\t\t\t},\n\t\t\tpath:  \"ns1/p1|c1|8080:8080\",\n\t\t\tkills: 1,\n\t\t},\n\t\t\"full_no_match_co\": {\n\t\t\tff: watch.Forwarders{\n\t\t\t\t\"ns1/p1|c1|8080:8080\":   newNoOpForwarder(),\n\t\t\t\t\"ns1/p1_1|c1|8080:8080\": newNoOpForwarder(),\n\t\t\t\t\"ns1/p2|c1|8080:8080\":   newNoOpForwarder(),\n\t\t\t},\n\t\t\tpath: \"ns1/p1|c2|8080:8080\",\n\t\t},\n\t\t\"full_no_match_ports\": {\n\t\t\tff: watch.Forwarders{\n\t\t\t\t\"ns1/p1|c1|8080:8080\":   newNoOpForwarder(),\n\t\t\t\t\"ns1/p1_1|c1|8080:8080\": newNoOpForwarder(),\n\t\t\t\t\"ns1/p2|c1|8080:8080\":   newNoOpForwarder(),\n\t\t\t},\n\t\t\tpath: \"ns1/p1|c1|8081:8080\",\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.kills, u.ff.Kill(u.path))\n\t\t})\n\t}\n}\n\ntype noOpForwarder struct{}\n\nfunc newNoOpForwarder() noOpForwarder {\n\treturn noOpForwarder{}\n}\n\nfunc (noOpForwarder) Start(string, port.PortTunnel) (*portforward.PortForwarder, error) {\n\treturn nil, nil\n}\nfunc (noOpForwarder) Stop()                      {}\nfunc (noOpForwarder) ID() string                 { return \"\" }\nfunc (noOpForwarder) Container() string          { return \"\" }\nfunc (noOpForwarder) Port() string               { return \"\" }\nfunc (noOpForwarder) FQN() string                { return \"\" }\nfunc (noOpForwarder) Active() bool               { return false }\nfunc (noOpForwarder) SetActive(bool)             {}\nfunc (noOpForwarder) Age() time.Time             { return time.Now() }\nfunc (noOpForwarder) HasPortMapping(string) bool { return false }\nfunc (noOpForwarder) Address() string            { return \"\" }\n"
  },
  {
    "path": "internal/watch/helper.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage watch\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"path\"\n\t\"strings\"\n\n\t\"k8s.io/apimachinery/pkg/runtime/schema\"\n)\n\nfunc toGVR(gvr string) schema.GroupVersionResource {\n\ttokens := strings.Split(gvr, \"/\")\n\tif len(tokens) < 3 {\n\t\ttokens = append([]string{\"\"}, tokens...)\n\t}\n\n\treturn schema.GroupVersionResource{\n\t\tGroup:    tokens[0],\n\t\tVersion:  tokens[1],\n\t\tResource: tokens[2],\n\t}\n}\n\nfunc namespaced(n string) (ns, res string) {\n\tns, res = path.Split(n)\n\n\treturn strings.Trim(ns, \"/\"), res\n}\n\n// DumpFactory for debug.\nfunc DumpFactory(f *Factory) {\n\tslog.Debug(\"----------- FACTORIES -------------\")\n\tfor ns := range f.factories {\n\t\tslog.Debug(fmt.Sprintf(\"  Factory for NS %q\", ns))\n\t}\n\tslog.Debug(\"-----------------------------------\")\n}\n\n// DebugFactory for debug.\nfunc DebugFactory(f *Factory, ns, gvr string) {\n\tslog.Debug(fmt.Sprintf(\"----------- DEBUG FACTORY (%s) -------------\", gvr))\n\tfac, ok := f.factories[ns]\n\tif !ok {\n\t\treturn\n\t}\n\tinf := fac.ForResource(toGVR(gvr))\n\tfor i, k := range inf.Informer().GetStore().ListKeys() {\n\t\tslog.Debug(fmt.Sprintf(\"%d -- %s\", i, k))\n\t}\n}\n"
  },
  {
    "path": "internal/xray/container.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"log/slog\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/slogs\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n)\n\n// Container represents an xray renderer.\ntype Container struct{}\n\n// Render renders an xray node.\nfunc (c *Container) Render(ctx context.Context, ns string, o any) error {\n\tco, ok := o.(render.ContainerRes)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected ContainerRes, but got %T\", o)\n\t}\n\n\tf, ok := ctx.Value(internal.KeyFactory).(dao.Factory)\n\tif !ok {\n\t\treturn fmt.Errorf(\"no factory found in context\")\n\t}\n\n\troot := NewTreeNode(client.CoGVR, client.FQN(ns, co.Container.Name))\n\tparent, ok := ctx.Value(KeyParent).(*TreeNode)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting a TreeNode but got %T\", ctx.Value(KeyParent))\n\t}\n\tpns, _ := client.Namespaced(parent.ID)\n\tc.envRefs(f, root, pns, co.Container)\n\tparent.Add(root)\n\n\treturn nil\n}\n\nfunc (c *Container) envRefs(f dao.Factory, parent *TreeNode, ns string, co *v1.Container) {\n\tfor _, e := range co.Env {\n\t\tif e.ValueFrom == nil {\n\t\t\tcontinue\n\t\t}\n\t\tc.secretRefs(f, parent, ns, e.ValueFrom.SecretKeyRef)\n\t\tc.configMapRefs(f, parent, ns, e.ValueFrom.ConfigMapKeyRef)\n\t}\n\n\tfor _, e := range co.EnvFrom {\n\t\tif e.ConfigMapRef != nil {\n\t\t\tgvr, id := client.CmGVR, client.FQN(ns, e.ConfigMapRef.Name)\n\t\t\taddRef(f, parent, gvr, id, e.ConfigMapRef.Optional)\n\t\t}\n\t\tif e.SecretRef != nil {\n\t\t\tgvr, id := client.SecGVR, client.FQN(ns, e.SecretRef.Name)\n\t\t\taddRef(f, parent, gvr, id, e.SecretRef.Optional)\n\t\t}\n\t}\n}\n\nfunc (c *Container) secretRefs(f dao.Factory, parent *TreeNode, ns string, ref *v1.SecretKeySelector) {\n\tif ref == nil {\n\t\treturn\n\t}\n\tgvr, id := client.SecGVR, client.FQN(ns, ref.Name)\n\taddRef(f, parent, gvr, id, ref.Optional)\n}\n\nfunc (c *Container) configMapRefs(f dao.Factory, parent *TreeNode, ns string, ref *v1.ConfigMapKeySelector) {\n\tif ref == nil {\n\t\treturn\n\t}\n\tgvr, id := client.CmGVR, client.FQN(ns, ref.Name)\n\taddRef(f, parent, gvr, id, ref.Optional)\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc addRef(f dao.Factory, parent *TreeNode, gvr *client.GVR, id string, optional *bool) {\n\tif parent.Find(gvr, id) == nil {\n\t\tn := NewTreeNode(gvr, id)\n\t\tvalidate(f, n, optional)\n\t\tparent.Add(n)\n\t}\n}\n\nfunc validate(f dao.Factory, n *TreeNode, optional *bool) {\n\tres, err := f.Get(n.GVR, n.ID, true, labels.Everything())\n\tif err != nil || res == nil {\n\t\tif optional == nil || !*optional {\n\t\t\tslog.Warn(\"Missing ref\",\n\t\t\t\tslogs.GVR, n.GVR,\n\t\t\t\tslogs.ID, n.ID,\n\t\t\t)\n\t\t\tn.Extras[StatusKey] = MissingRefStatus\n\t\t}\n\t\treturn\n\t}\n\tn.Extras[StatusKey] = OkStatus\n}\n"
  },
  {
    "path": "internal/xray/container_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray_test\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log/slog\"\n\t\"os\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/watch\"\n\t\"github.com/derailed/k9s/internal/xray\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n\t\"k8s.io/client-go/informers\"\n)\n\nfunc init() {\n\tslog.SetDefault(slog.New(slog.DiscardHandler))\n}\n\nfunc TestCOConfigMapRefs(t *testing.T) {\n\tvar re xray.Container\n\n\troot := xray.NewTreeNode(client.NewGVR(\"root\"), \"root\")\n\tctx := context.WithValue(context.Background(), xray.KeyParent, root)\n\tctx = context.WithValue(ctx, internal.KeyFactory, makeFactory())\n\n\trequire.NoError(t, re.Render(ctx, \"\", render.ContainerRes{Container: makeCMContainer(\"c1\", false)}))\n\tassert.Equal(t, xray.MissingRefStatus, root.Children[0].Children[0].Extras[xray.StatusKey])\n}\n\nfunc TestCORefs(t *testing.T) {\n\tuu := map[string]struct {\n\t\tco             render.ContainerRes\n\t\tlevel1, level2 int\n\t\te              string\n\t}{\n\t\t\"cm_required\": {\n\t\t\tco:     render.ContainerRes{Container: makeCMContainer(\"c1\", false)},\n\t\t\tlevel1: 1,\n\t\t\tlevel2: 1,\n\t\t\te:      xray.MissingRefStatus,\n\t\t},\n\t\t\"cm_optional\": {\n\t\t\tco:     render.ContainerRes{Container: makeCMContainer(\"c1\", true)},\n\t\t\tlevel1: 1,\n\t\t\tlevel2: 1,\n\t\t\te:      xray.OkStatus,\n\t\t},\n\t\t\"cm_doubleRef\": {\n\t\t\tco:     render.ContainerRes{Container: makeDoubleCMKeysContainer(\"c1\", false)},\n\t\t\tlevel1: 1,\n\t\t\tlevel2: 1,\n\t\t\te:      xray.MissingRefStatus,\n\t\t},\n\t\t\"sec_required\": {\n\t\t\tco:     render.ContainerRes{Container: makeSecContainer(\"c1\", false)},\n\t\t\tlevel1: 1,\n\t\t\tlevel2: 1,\n\t\t\te:      xray.MissingRefStatus,\n\t\t},\n\t\t\"sec_optional\": {\n\t\t\tco:     render.ContainerRes{Container: makeSecContainer(\"c1\", true)},\n\t\t\tlevel1: 1,\n\t\t\tlevel2: 1,\n\t\t\te:      xray.OkStatus,\n\t\t},\n\t\t\"envFrom_optional\": {\n\t\t\tco:     render.ContainerRes{Container: makeCMEnvFromContainer(\"c1\", false)},\n\t\t\tlevel1: 1,\n\t\t\tlevel2: 2,\n\t\t\te:      xray.MissingRefStatus,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tvar re xray.Container\n\t\t\troot := xray.NewTreeNode(client.NewGVR(\"root\"), \"root\")\n\t\t\tctx := context.WithValue(context.Background(), xray.KeyParent, root)\n\t\t\tctx = context.WithValue(ctx, internal.KeyFactory, makeFactory())\n\n\t\t\trequire.NoError(t, re.Render(ctx, \"\", u.co))\n\t\t\tassert.Equal(t, u.level1, root.CountChildren())\n\t\t\tassert.Equal(t, u.level2, root.Children[0].CountChildren())\n\t\t\tassert.Equal(t, u.e, root.Children[0].Children[0].Extras[xray.StatusKey])\n\t\t})\n\t}\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc makeFactory() testFactory {\n\treturn testFactory{}\n}\n\ntype testFactory struct {\n\trows map[*client.GVR][]runtime.Object\n}\n\nvar _ dao.Factory = testFactory{}\n\nfunc (f testFactory) Client() client.Connection {\n\treturn nil\n}\n\nfunc (f testFactory) Get(gvr *client.GVR, _ string, _ bool, _ labels.Selector) (runtime.Object, error) {\n\too, ok := f.rows[gvr]\n\tif ok && len(oo) > 0 {\n\t\treturn oo[0], nil\n\t}\n\treturn nil, nil\n}\n\nfunc (f testFactory) List(gvr *client.GVR, _ string, _ bool, _ labels.Selector) ([]runtime.Object, error) {\n\too, ok := f.rows[gvr]\n\tif ok {\n\t\treturn oo, nil\n\t}\n\treturn nil, nil\n}\n\nfunc (f testFactory) ForResource(string, *client.GVR) (informers.GenericInformer, error) {\n\treturn nil, nil\n}\n\nfunc (f testFactory) CanForResource(string, *client.GVR, []string) (informers.GenericInformer, error) {\n\treturn nil, nil\n}\nfunc (f testFactory) WaitForCacheSync() {}\nfunc (f testFactory) Forwarders() watch.Forwarders {\n\treturn nil\n}\nfunc (f testFactory) DeleteForwarder(string) {}\n\nfunc makeCMEnvFromContainer(n string, optional bool) *v1.Container {\n\treturn &v1.Container{\n\t\tName: n,\n\t\tEnvFrom: []v1.EnvFromSource{\n\t\t\t{\n\t\t\t\tConfigMapRef: &v1.ConfigMapEnvSource{\n\t\t\t\t\tLocalObjectReference: v1.LocalObjectReference{\n\t\t\t\t\t\tName: \"cm1\",\n\t\t\t\t\t},\n\t\t\t\t\tOptional: &optional,\n\t\t\t\t},\n\t\t\t\tSecretRef: &v1.SecretEnvSource{\n\t\t\t\t\tLocalObjectReference: v1.LocalObjectReference{\n\t\t\t\t\t\tName: \"sec1\",\n\t\t\t\t\t},\n\t\t\t\t\tOptional: &optional,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc makeCMContainer(n string, optional bool) *v1.Container {\n\treturn &v1.Container{\n\t\tName: n,\n\t\tEnv: []v1.EnvVar{\n\t\t\t{\n\t\t\t\tName: \"e1\",\n\t\t\t\tValueFrom: &v1.EnvVarSource{\n\t\t\t\t\tConfigMapKeyRef: &v1.ConfigMapKeySelector{\n\t\t\t\t\t\tLocalObjectReference: v1.LocalObjectReference{\n\t\t\t\t\t\t\tName: \"cm1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tKey:      \"k1\",\n\t\t\t\t\t\tOptional: &optional,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc makeSecContainer(n string, optional bool) *v1.Container {\n\treturn &v1.Container{\n\t\tName: n,\n\t\tEnv: []v1.EnvVar{\n\t\t\t{\n\t\t\t\tName: \"e1\",\n\t\t\t\tValueFrom: &v1.EnvVarSource{\n\t\t\t\t\tSecretKeyRef: &v1.SecretKeySelector{\n\t\t\t\t\t\tLocalObjectReference: v1.LocalObjectReference{\n\t\t\t\t\t\t\tName: \"sec1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tKey:      \"k1\",\n\t\t\t\t\t\tOptional: &optional,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc makeDoubleCMKeysContainer(n string, optional bool) *v1.Container {\n\treturn &v1.Container{\n\t\tName: n,\n\t\tEnv: []v1.EnvVar{\n\t\t\t{\n\t\t\t\tName: \"e1\",\n\t\t\t\tValueFrom: &v1.EnvVarSource{\n\t\t\t\t\tConfigMapKeyRef: &v1.ConfigMapKeySelector{\n\t\t\t\t\t\tLocalObjectReference: v1.LocalObjectReference{\n\t\t\t\t\t\t\tName: \"cm1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tKey:      \"k2\",\n\t\t\t\t\t\tOptional: &optional,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: \"e2\",\n\t\t\t\tValueFrom: &v1.EnvVarSource{\n\t\t\t\t\tConfigMapKeyRef: &v1.ConfigMapKeySelector{\n\t\t\t\t\t\tLocalObjectReference: v1.LocalObjectReference{\n\t\t\t\t\t\t\tName: \"cm1\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tKey:      \"k1\",\n\t\t\t\t\t\tOptional: &optional,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc load(t *testing.T, n string) *unstructured.Unstructured {\n\traw, err := os.ReadFile(fmt.Sprintf(\"testdata/%s.json\", n))\n\trequire.NoError(t, err)\n\n\tvar o unstructured.Unstructured\n\terr = json.Unmarshal(raw, &o)\n\trequire.NoError(t, err)\n\n\treturn &o\n}\n"
  },
  {
    "path": "internal/xray/dp.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/render\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\n// Deployment represents an xray renderer.\ntype Deployment struct{}\n\n// Render renders an xray node.\nfunc (d *Deployment) Render(ctx context.Context, ns string, o any) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tvar dp appsv1.Deployment\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &dp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tparent, ok := ctx.Value(KeyParent).(*TreeNode)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting a TreeNode but got %T\", ctx.Value(KeyParent))\n\t}\n\n\troot := NewTreeNode(client.DpGVR, client.FQN(dp.Namespace, dp.Name))\n\too, err := locatePods(ctx, dp.Namespace, dp.Spec.Selector)\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx = context.WithValue(ctx, KeyParent, root)\n\tvar re Pod\n\tfor _, o := range oo {\n\t\tp, ok := o.(*unstructured.Unstructured)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"expecting *Unstructured but got %T\", o)\n\t\t}\n\t\tif err := re.Render(ctx, ns, &render.PodWithMetrics{Raw: p}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif root.IsLeaf() {\n\t\treturn nil\n\t}\n\tgvr, nsID := client.NsGVR, client.FQN(client.ClusterScope, dp.Namespace)\n\tnsn := parent.Find(gvr, nsID)\n\tif nsn == nil {\n\t\tnsn = NewTreeNode(gvr, nsID)\n\t\tparent.Add(nsn)\n\t}\n\tnsn.Add(root)\n\n\treturn d.validate(root, dp)\n}\n\nfunc (*Deployment) validate(root *TreeNode, dp appsv1.Deployment) error {\n\troot.Extras[StatusKey] = OkStatus\n\tvar r int32\n\tif dp.Spec.Replicas != nil {\n\t\tr = int32(*dp.Spec.Replicas)\n\t}\n\ta := dp.Status.AvailableReplicas\n\tif a != r || dp.Status.UnavailableReplicas != 0 {\n\t\troot.Extras[StatusKey] = ToastStatus\n\t}\n\troot.Extras[InfoKey] = fmt.Sprintf(\"%d/%d/%d\", a, r, dp.Status.UnavailableReplicas)\n\n\treturn nil\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc locatePods(ctx context.Context, ns string, sel *metav1.LabelSelector) ([]runtime.Object, error) {\n\tl, err := metav1.LabelSelectorAsSelector(sel)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tfsel, err := labels.ConvertSelectorToLabelsMap(l.String())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tf, ok := ctx.Value(internal.KeyFactory).(dao.Factory)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"expecting a factory but got %T\", ctx.Value(internal.KeyFactory))\n\t}\n\n\treturn f.List(client.PodGVR, ns, false, fsel.AsSelector())\n}\n"
  },
  {
    "path": "internal/xray/dp_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/xray\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nfunc TestDeployRender(t *testing.T) {\n\tuu := map[string]struct {\n\t\tfile           string\n\t\tlevel1, level2 int\n\t\tstatus         string\n\t}{\n\t\t\"plain\": {\n\t\t\tfile:   \"dp\",\n\t\t\tlevel1: 1,\n\t\t\tlevel2: 1,\n\t\t\tstatus: xray.OkStatus,\n\t\t},\n\t}\n\n\tvar re xray.Deployment\n\tfor k := range uu {\n\t\tf := makeFactory()\n\t\tf.rows = map[*client.GVR][]runtime.Object{\n\t\t\tclient.PodGVR: {load(t, \"po\")},\n\t\t\tclient.SaGVR:  {load(t, \"sa\")},\n\t\t}\n\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\to := load(t, u.file)\n\t\t\troot := xray.NewTreeNode(client.DpGVR, \"deployments\")\n\t\t\tctx := context.WithValue(context.Background(), xray.KeyParent, root)\n\t\t\tctx = context.WithValue(ctx, internal.KeyFactory, f)\n\n\t\t\trequire.NoError(t, re.Render(ctx, \"\", o))\n\t\t\tassert.Equal(t, u.level1, root.CountChildren())\n\t\t\tassert.Equal(t, u.level2, root.Children[0].CountChildren())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/xray/ds.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\n// DaemonSet represents an xray renderer.\ntype DaemonSet struct{}\n\n// Render renders an xray node.\nfunc (d *DaemonSet) Render(ctx context.Context, ns string, o any) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tvar ds appsv1.DaemonSet\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ds)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tparent, ok := ctx.Value(KeyParent).(*TreeNode)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting a TreeNode but got %T\", ctx.Value(KeyParent))\n\t}\n\n\troot := NewTreeNode(client.DsGVR, client.FQN(ds.Namespace, ds.Name))\n\too, err := locatePods(ctx, ds.Namespace, ds.Spec.Selector)\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx = context.WithValue(ctx, KeyParent, root)\n\tvar re Pod\n\tfor _, o := range oo {\n\t\tp, ok := o.(*unstructured.Unstructured)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"expecting *Unstructured but got %T\", o)\n\t\t}\n\t\tif err := re.Render(ctx, ns, &render.PodWithMetrics{Raw: p}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif root.IsLeaf() {\n\t\treturn nil\n\t}\n\tgvr, nsID := client.NsGVR, client.FQN(client.ClusterScope, ds.Namespace)\n\tnsn := parent.Find(gvr, nsID)\n\tif nsn == nil {\n\t\tnsn = NewTreeNode(gvr, nsID)\n\t\tparent.Add(nsn)\n\t}\n\tnsn.Add(root)\n\n\treturn d.validate(root, ds)\n}\n\nfunc (*DaemonSet) validate(root *TreeNode, ds appsv1.DaemonSet) error {\n\troot.Extras[StatusKey] = OkStatus\n\td := ds.Status.DesiredNumberScheduled\n\ta := ds.Status.NumberAvailable\n\tif d != a {\n\t\troot.Extras[StatusKey] = ToastStatus\n\t}\n\troot.Extras[InfoKey] = fmt.Sprintf(\"%d/%d\", a, d)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/xray/ds_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/xray\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nfunc TestDaemonSetRender(t *testing.T) {\n\tuu := map[string]struct {\n\t\tfile           string\n\t\tlevel1, level2 int\n\t\tstatus         string\n\t}{\n\t\t\"plain\": {\n\t\t\tfile:   \"ds\",\n\t\t\tlevel1: 1,\n\t\t\tlevel2: 1,\n\t\t\tstatus: xray.OkStatus,\n\t\t},\n\t}\n\n\tvar re xray.DaemonSet\n\tfor k := range uu {\n\t\tf := makeFactory()\n\t\tf.rows = map[*client.GVR][]runtime.Object{client.PodGVR: {load(t, \"po\")}}\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\to := load(t, u.file)\n\t\t\troot := xray.NewTreeNode(client.DsGVR, \"daemonsets\")\n\t\t\tctx := context.WithValue(context.Background(), xray.KeyParent, root)\n\t\t\tctx = context.WithValue(ctx, internal.KeyFactory, f)\n\n\t\t\trequire.NoError(t, re.Render(ctx, \"\", o))\n\t\t\tassert.Equal(t, u.level1, root.CountChildren())\n\t\t\tassert.Equal(t, u.level2, root.Children[0].CountChildren())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/xray/generic.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\tmetav1 \"k8s.io/apimachinery/pkg/apis/meta/v1\"\n)\n\n// Generic renders a generic resource to screen.\ntype Generic struct {\n\ttable *metav1.Table\n}\n\n// SetTable sets the tabular resource.\nfunc (g *Generic) SetTable(_ string, t *metav1.Table) {\n\tg.table = t\n}\n\n// Render renders a K8s resource to screen.\nfunc (g *Generic) Render(ctx context.Context, ns string, o any) error {\n\trow, ok := o.(metav1.TableRow)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting a TableRow but got %T\", o)\n\t}\n\n\tn, ok := row.Cells[0].(string)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting row 0 to be a string but got %T\", row.Cells[0])\n\t}\n\n\troot := NewTreeNode(client.NewGVR(\"generic\"), client.FQN(ns, n))\n\tparent, ok := ctx.Value(KeyParent).(*TreeNode)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting TreeNode but got %T\", ctx.Value(KeyParent))\n\t}\n\tparent.Add(root)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/xray/generic_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/xray\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\tmetav1beta1 \"k8s.io/apimachinery/pkg/apis/meta/v1beta1\"\n)\n\nfunc TestGenericRender(t *testing.T) {\n\tuu := map[string]struct {\n\t\tlevel1 int\n\t\tstatus string\n\t}{\n\t\t\"plain\": {\n\t\t\tlevel1: 1,\n\t\t\tstatus: xray.OkStatus,\n\t\t},\n\t}\n\n\tvar re xray.Generic\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\troot := xray.NewTreeNode(client.NewGVR(\"generic\"), \"generics\")\n\t\t\tctx := context.WithValue(context.Background(), xray.KeyParent, root)\n\t\t\tctx = context.WithValue(ctx, internal.KeyFactory, makeFactory())\n\n\t\t\trequire.NoError(t, re.Render(ctx, \"\", makeTable()))\n\t\t\tassert.Equal(t, u.level1, root.CountChildren())\n\t\t})\n\t}\n}\n\n// Helpers...\n\nfunc makeTable() metav1beta1.TableRow {\n\treturn metav1beta1.TableRow{\n\t\tCells: []any{\"fred\", \"blee\"},\n\t}\n}\n"
  },
  {
    "path": "internal/xray/ns.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\n// Namespace represents an xray renderer.\ntype Namespace struct{}\n\n// Render renders an xray node.\nfunc (n *Namespace) Render(ctx context.Context, ns string, o any) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected NamespaceWithMetrics, but got %T\", o)\n\t}\n\n\tvar nss v1.Namespace\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &nss)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\troot := NewTreeNode(client.NsGVR, client.FQN(client.ClusterScope, nss.Name))\n\tparent, ok := ctx.Value(KeyParent).(*TreeNode)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting a TreeNode but got %T\", ctx.Value(KeyParent))\n\t}\n\tparent.Add(root)\n\n\treturn n.validate(root, nss)\n}\n\nfunc (*Namespace) validate(root *TreeNode, ns v1.Namespace) error {\n\troot.Extras[StatusKey] = OkStatus\n\tif ns.Status.Phase == v1.NamespaceTerminating {\n\t\troot.Extras[StatusKey] = ToastStatus\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/xray/ns_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/xray\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNamespaceRender(t *testing.T) {\n\tuu := map[string]struct {\n\t\tfile   string\n\t\tlevel1 int\n\t\tstatus string\n\t}{\n\t\t\"plain\": {\n\t\t\tfile:   \"ns\",\n\t\t\tlevel1: 1,\n\t\t\tstatus: xray.OkStatus,\n\t\t},\n\t}\n\n\tvar re xray.Namespace\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\to := load(t, u.file)\n\t\t\troot := xray.NewTreeNode(client.NsGVR, \"namespaces\")\n\t\t\tctx := context.WithValue(context.Background(), xray.KeyParent, root)\n\t\t\tctx = context.WithValue(ctx, internal.KeyFactory, makeFactory())\n\n\t\t\trequire.NoError(t, re.Render(ctx, \"\", o))\n\t\t\tassert.Equal(t, u.level1, root.CountChildren())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/xray/pod.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/render\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\n// Pod represents an xray renderer.\ntype Pod struct{}\n\n// Render renders an xray node.\nfunc (p *Pod) Render(ctx context.Context, ns string, o any) error {\n\tpwm, ok := o.(*render.PodWithMetrics)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected PodWithMetrics, but got %T\", o)\n\t}\n\n\tvar po v1.Pod\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(pwm.Raw.Object, &po)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf, ok := ctx.Value(internal.KeyFactory).(dao.Factory)\n\tif !ok {\n\t\treturn fmt.Errorf(\"no factory found in context\")\n\t}\n\n\tnode := NewTreeNode(client.PodGVR, client.FQN(po.Namespace, po.Name))\n\tparent, ok := ctx.Value(KeyParent).(*TreeNode)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting a TreeNode but got %T\", ctx.Value(KeyParent))\n\t}\n\n\tif err := p.containerRefs(ctx, node, po.Namespace, &po.Spec); err != nil {\n\t\treturn err\n\t}\n\tp.podVolumeRefs(f, node, po.Namespace, po.Spec.Volumes)\n\tif err := p.serviceAccountRef(ctx, f, node, po.Namespace, &po.Spec); err != nil {\n\t\treturn err\n\t}\n\n\tgvr, nsID := client.NsGVR, client.FQN(client.ClusterScope, po.Namespace)\n\tnsn := parent.Find(gvr, nsID)\n\tif nsn == nil {\n\t\tnsn = NewTreeNode(gvr, nsID)\n\t\tparent.Add(nsn)\n\t}\n\tnsn.Add(node)\n\n\treturn p.validate(node, po)\n}\n\nfunc (p *Pod) validate(node *TreeNode, po v1.Pod) error {\n\tvar re render.Pod\n\tphase := re.Phase(po.DeletionTimestamp, &po.Spec, &po.Status)\n\tss := po.Status.ContainerStatuses\n\treadyCnt, _, _, _ := re.ContainerStats(ss)\n\tstatus := OkStatus\n\tif readyCnt != len(ss) {\n\t\tstatus = ToastStatus\n\t}\n\tif phase == \"Completed\" {\n\t\tstatus = CompletedStatus\n\t}\n\n\tnode.Extras[StatusKey] = status\n\tnode.Extras[InfoKey] = strconv.Itoa(readyCnt) + \"/\" + strconv.Itoa(len(ss))\n\n\treturn nil\n}\n\nfunc (*Pod) containerRefs(ctx context.Context, parent *TreeNode, ns string, spec *v1.PodSpec) error {\n\tctx = context.WithValue(ctx, KeyParent, parent)\n\tvar cre Container\n\tfor i := range len(spec.InitContainers) {\n\t\tif err := cre.Render(ctx, ns, render.ContainerRes{Container: &spec.InitContainers[i]}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tfor i := range len(spec.Containers) {\n\t\tif err := cre.Render(ctx, ns, render.ContainerRes{Container: &spec.Containers[i]}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tfor i := range len(spec.EphemeralContainers) {\n\t\tif err := cre.Render(ctx, ns, render.ContainerRes{Container: &spec.Containers[i]}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (*Pod) serviceAccountRef(ctx context.Context, f dao.Factory, parent *TreeNode, ns string, spec *v1.PodSpec) error {\n\tif spec.ServiceAccountName == \"\" {\n\t\treturn nil\n\t}\n\n\tfqn := client.FQN(ns, spec.ServiceAccountName)\n\to, err := f.Get(client.SaGVR, fqn, true, labels.Everything())\n\tif err != nil {\n\t\treturn err\n\t}\n\tif o == nil {\n\t\taddRef(f, parent, client.SaGVR, fqn, nil)\n\t\treturn nil\n\t}\n\n\tvar saRE ServiceAccount\n\tctx = context.WithValue(ctx, KeyParent, parent)\n\tctx = context.WithValue(ctx, KeySAAutomount, spec.AutomountServiceAccountToken)\n\treturn saRE.Render(ctx, ns, o)\n}\n\nfunc (*Pod) podVolumeRefs(f dao.Factory, parent *TreeNode, ns string, vv []v1.Volume) {\n\tfor i := range vv {\n\t\tsec := vv[i].Secret\n\t\tif sec != nil {\n\t\t\taddRef(f, parent, client.SecGVR, client.FQN(ns, sec.SecretName), sec.Optional)\n\t\t\tcontinue\n\t\t}\n\n\t\tcm := vv[i].ConfigMap\n\t\tif cm != nil {\n\t\t\taddRef(f, parent, client.CmGVR, client.FQN(ns, cm.Name), cm.Optional)\n\t\t\tcontinue\n\t\t}\n\n\t\tpvc := vv[i].PersistentVolumeClaim\n\t\tif pvc != nil {\n\t\t\taddRef(f, parent, client.PvcGVR, client.FQN(ns, pvc.ClaimName), nil)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/xray/pod_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\t\"github.com/derailed/k9s/internal/xray\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPodRender(t *testing.T) {\n\tuu := map[string]struct {\n\t\tfile            string\n\t\tcount, children int\n\t\tstatus          string\n\t}{\n\t\t\"plain\": {\n\t\t\tfile:     \"po\",\n\t\t\tchildren: 1,\n\t\t\tcount:    7,\n\t\t\tstatus:   xray.OkStatus,\n\t\t},\n\t\t\"withInit\": {\n\t\t\tfile:     \"init\",\n\t\t\tchildren: 1,\n\t\t\tcount:    7,\n\t\t\tstatus:   xray.OkStatus,\n\t\t},\n\t\t\"cilium\": {\n\t\t\tfile:     \"cilium\",\n\t\t\tchildren: 1,\n\t\t\tcount:    8,\n\t\t\tstatus:   xray.OkStatus,\n\t\t},\n\t}\n\n\tvar re xray.Pod\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\to := load(t, u.file)\n\t\t\troot := xray.NewTreeNode(client.PodGVR, \"pods\")\n\t\t\tctx := context.WithValue(context.Background(), xray.KeyParent, root)\n\t\t\tctx = context.WithValue(ctx, internal.KeyFactory, makeFactory())\n\n\t\t\trequire.NoError(t, re.Render(ctx, \"\", &render.PodWithMetrics{Raw: o}))\n\t\t\tassert.Equal(t, u.children, root.CountChildren())\n\t\t\tassert.Equal(t, u.count, root.Count(client.NoGVR))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/xray/rs.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\n// ReplicaSet represents an xray renderer.\ntype ReplicaSet struct{}\n\n// Render renders an xray node.\nfunc (r *ReplicaSet) Render(ctx context.Context, ns string, o any) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tvar rs appsv1.ReplicaSet\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &rs)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tparent, ok := ctx.Value(KeyParent).(*TreeNode)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting a TreeNode but got %T\", ctx.Value(KeyParent))\n\t}\n\n\troot := NewTreeNode(client.RsGVR, client.FQN(rs.Namespace, rs.Name))\n\too, err := locatePods(ctx, rs.Namespace, rs.Spec.Selector)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tctx = context.WithValue(ctx, KeyParent, root)\n\tvar re Pod\n\tfor _, o := range oo {\n\t\tp, ok := o.(*unstructured.Unstructured)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"expecting *Unstructured but got %T\", o)\n\t\t}\n\t\tif err := re.Render(ctx, ns, &render.PodWithMetrics{Raw: p}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif root.IsLeaf() {\n\t\treturn nil\n\t}\n\n\tgvr, nsID := client.NsGVR, client.FQN(client.ClusterScope, rs.Namespace)\n\tnsn := parent.Find(gvr, nsID)\n\tif nsn == nil {\n\t\tnsn = NewTreeNode(gvr, nsID)\n\t\tparent.Add(nsn)\n\t}\n\tnsn.Add(root)\n\n\treturn r.validate(root, rs)\n}\n\nfunc (*ReplicaSet) validate(root *TreeNode, rs appsv1.ReplicaSet) error {\n\troot.Extras[StatusKey] = OkStatus\n\tvar r int32\n\tif rs.Spec.Replicas != nil {\n\t\tr = int32(*rs.Spec.Replicas)\n\t}\n\ta := rs.Status.Replicas\n\tif a != r {\n\t\troot.Extras[StatusKey] = ToastStatus\n\t}\n\troot.Extras[InfoKey] = fmt.Sprintf(\"%d/%d\", a, r)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/xray/rs_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/xray\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nfunc TestReplicaSetRender(t *testing.T) {\n\tuu := map[string]struct {\n\t\tfile           string\n\t\tlevel1, level2 int\n\t\tstatus         string\n\t}{\n\t\t\"plain\": {\n\t\t\tfile:   \"rs\",\n\t\t\tlevel1: 1,\n\t\t\tlevel2: 1,\n\t\t\tstatus: xray.OkStatus,\n\t\t},\n\t}\n\n\tvar re xray.ReplicaSet\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tf := makeFactory()\n\t\t\tf.rows = map[*client.GVR][]runtime.Object{\n\t\t\t\tclient.PodGVR: {load(t, \"po\")},\n\t\t\t}\n\n\t\t\to := load(t, u.file)\n\t\t\troot := xray.NewTreeNode(client.RsGVR, \"replicasets\")\n\t\t\tctx := context.WithValue(context.Background(), xray.KeyParent, root)\n\t\t\tctx = context.WithValue(ctx, internal.KeyFactory, f)\n\n\t\t\trequire.NoError(t, re.Render(ctx, \"\", o))\n\t\t\tassert.Equal(t, u.level1, root.CountChildren())\n\t\t\tassert.Equal(t, u.level2, root.Children[0].CountChildren())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/xray/sa.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\n// ServiceAccount represents an xray renderer.\ntype ServiceAccount struct{}\n\n// Render renders an xray node.\nfunc (s *ServiceAccount) Render(ctx context.Context, ns string, o any) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"ServiceAccount render expecting *Unstructured, but got %T\", o)\n\t}\n\n\tvar sa v1.ServiceAccount\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sa)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tf, ok := ctx.Value(internal.KeyFactory).(dao.Factory)\n\tif !ok {\n\t\treturn fmt.Errorf(\"no factory found in context\")\n\t}\n\tnode := NewTreeNode(client.SaGVR, client.FQN(sa.Namespace, sa.Name))\n\n\tparent, ok := ctx.Value(KeyParent).(*TreeNode)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting a TreeNode but got %T\", ctx.Value(KeyParent))\n\t}\n\tparent.Add(node)\n\n\tfor _, sec := range sa.Secrets {\n\t\taddRef(f, node, client.SecGVR, client.FQN(sa.Namespace, sec.Name), nil)\n\t}\n\tfor _, sec := range sa.ImagePullSecrets {\n\t\taddRef(f, node, client.SecGVR, client.FQN(sa.Namespace, sec.Name), nil)\n\t}\n\n\tauto, _ := ctx.Value(KeySAAutomount).(*bool)\n\treturn s.validate(node, sa, auto)\n}\n\nfunc (*ServiceAccount) validate(node *TreeNode, sa v1.ServiceAccount, auto *bool) error {\n\tnode.Extras[StatusKey] = OkStatus\n\tif sa.AutomountServiceAccountToken != nil {\n\t\tnode.Extras[InfoKey] = fmt.Sprintf(\"automount=%t\", *sa.AutomountServiceAccountToken)\n\t}\n\tif auto != nil {\n\t\tnode.Extras[InfoKey] = fmt.Sprintf(\"automount=%t\", *auto)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/xray/sa_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/xray\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSARender(t *testing.T) {\n\tuu := map[string]struct {\n\t\tfile           string\n\t\tlevel1, level2 int\n\t\tstatus         string\n\t}{\n\t\t\"plain\": {\n\t\t\tfile:   \"sa\",\n\t\t\tlevel1: 1,\n\t\t\tlevel2: 2,\n\t\t\tstatus: xray.OkStatus,\n\t\t},\n\t}\n\n\tvar re xray.ServiceAccount\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\to := load(t, u.file)\n\t\t\troot := xray.NewTreeNode(client.SaGVR, \"serviceaccounts\")\n\t\t\tctx := context.WithValue(context.Background(), xray.KeyParent, root)\n\t\t\tctx = context.WithValue(ctx, internal.KeyFactory, makeFactory())\n\n\t\t\trequire.NoError(t, re.Render(ctx, \"\", o))\n\t\t\tassert.Equal(t, u.level1, root.CountChildren())\n\t\t\tassert.Equal(t, u.level2, root.Children[0].CountChildren())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/xray/section.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n)\n\n// Section represents an xray renderer.\ntype Section struct {\n\trender.Base\n}\n\n// Render renders an xray node.\nfunc (s *Section) Render(ctx context.Context, ns string, o any) error {\n\tsection, ok := o.(render.Section)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Section, but got %T\", o)\n\t}\n\troot := NewTreeNode(client.NewGVR(section.GVR), section.Title)\n\tparent, ok := ctx.Value(KeyParent).(*TreeNode)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting a TreeNode but got %T\", ctx.Value(KeyParent))\n\t}\n\ts.outcomeRefs(root, section)\n\tparent.Add(root)\n\n\treturn nil\n}\n\nfunc (*Section) outcomeRefs(parent *TreeNode, section render.Section) {\n\tfor k, issues := range section.Outcome {\n\t\tp := NewTreeNode(client.NewGVR(section.GVR), cleanse(k))\n\t\tparent.Add(p)\n\t\tfor _, issue := range issues {\n\t\t\tmsg := colorize(cleanse(issue.Message), issue.Level)\n\t\t\tc := NewTreeNode(client.NewGVR(fmt.Sprintf(\"issue_%d\", issue.Level)), msg)\n\t\t\tif issue.Group == \"__root__\" {\n\t\t\t\tp.Add(c)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif pa := p.Find(client.NewGVR(issue.GVR), issue.Group); pa != nil {\n\t\t\t\tpa.Add(c)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tpa := NewTreeNode(client.NewGVR(issue.GVR), issue.Group)\n\t\t\tpa.Add(c)\n\t\t\tp.Add(pa)\n\t\t}\n\t}\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc colorize(s string, l render.Level) string {\n\tc := \"green\"\n\t// nolint:exhaustive\n\tswitch l {\n\tcase render.ErrorLevel:\n\t\tc = \"red\"\n\tcase render.WarnLevel:\n\t\tc = \"orange\"\n\tcase render.InfoLevel:\n\t\tc = \"blue\"\n\t}\n\treturn fmt.Sprintf(\"[%s::]%s\", c, s)\n}\n\nfunc cleanse(s string) string {\n\ts = strings.ReplaceAll(s, \"[\", \"(\")\n\ts = strings.ReplaceAll(s, \"]\", \")\")\n\ts = strings.ReplaceAll(s, \"/\", \"::\")\n\treturn s\n}\n"
  },
  {
    "path": "internal/xray/sts.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/render\"\n\tappsv1 \"k8s.io/api/apps/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\n// StatefulSet represents an xray renderer.\ntype StatefulSet struct{}\n\n// Render renders an xray node.\nfunc (s *StatefulSet) Render(ctx context.Context, ns string, o any) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\tvar sts appsv1.StatefulSet\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tparent, ok := ctx.Value(KeyParent).(*TreeNode)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting a TreeNode but got %T\", ctx.Value(KeyParent))\n\t}\n\n\troot := NewTreeNode(client.StsGVR, client.FQN(sts.Namespace, sts.Name))\n\too, err := locatePods(ctx, sts.Namespace, sts.Spec.Selector)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tctx = context.WithValue(ctx, KeyParent, root)\n\tvar re Pod\n\tfor _, o := range oo {\n\t\tp, ok := o.(*unstructured.Unstructured)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"expecting *Unstructured but got %T\", o)\n\t\t}\n\t\tif err := re.Render(ctx, ns, &render.PodWithMetrics{Raw: p}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif root.IsLeaf() {\n\t\treturn nil\n\t}\n\n\tgvr, nsID := client.NsGVR, client.FQN(client.ClusterScope, sts.Namespace)\n\tnsn := parent.Find(gvr, nsID)\n\tif nsn == nil {\n\t\tnsn = NewTreeNode(gvr, nsID)\n\t\tparent.Add(nsn)\n\t}\n\tnsn.Add(root)\n\n\treturn s.validate(root, sts)\n}\n\nfunc (*StatefulSet) validate(root *TreeNode, sts appsv1.StatefulSet) error {\n\troot.Extras[StatusKey] = OkStatus\n\tvar r int32\n\tif sts.Spec.Replicas != nil {\n\t\tr = int32(*sts.Spec.Replicas)\n\t}\n\ta := sts.Status.CurrentReplicas\n\tif a != r {\n\t\troot.Extras[StatusKey] = ToastStatus\n\t}\n\troot.Extras[InfoKey] = fmt.Sprintf(\"%d/%d\", a, r)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/xray/sts_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/xray\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nfunc TestStatefulSetRender(t *testing.T) {\n\tuu := map[string]struct {\n\t\tfile           string\n\t\tlevel1, level2 int\n\t\tstatus         string\n\t}{\n\t\t\"plain\": {\n\t\t\tfile:   \"sts\",\n\t\t\tlevel1: 1,\n\t\t\tlevel2: 1,\n\t\t\tstatus: xray.OkStatus,\n\t\t},\n\t}\n\n\tvar re xray.StatefulSet\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tf := makeFactory()\n\t\t\tf.rows = map[*client.GVR][]runtime.Object{client.PodGVR: {load(t, \"po\")}}\n\n\t\t\to := load(t, u.file)\n\t\t\troot := xray.NewTreeNode(client.StsGVR, \"statefulsets\")\n\t\t\tctx := context.WithValue(context.Background(), xray.KeyParent, root)\n\t\t\tctx = context.WithValue(ctx, internal.KeyFactory, f)\n\n\t\t\trequire.NoError(t, re.Render(ctx, \"\", o))\n\t\t\tassert.Equal(t, u.level1, root.CountChildren())\n\t\t\tassert.Equal(t, u.level2, root.Children[0].CountChildren())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/xray/svc.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/derailed/k9s/internal/render\"\n\tv1 \"k8s.io/api/core/v1\"\n\t\"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured\"\n\t\"k8s.io/apimachinery/pkg/labels\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\n// Service represents an xray renderer.\ntype Service struct{}\n\n// Render renders an xray node.\nfunc (s *Service) Render(ctx context.Context, ns string, o any) error {\n\traw, ok := o.(*unstructured.Unstructured)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expected Unstructured, but got %T\", o)\n\t}\n\n\tvar svc v1.Service\n\terr := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &svc)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tparent, ok := ctx.Value(KeyParent).(*TreeNode)\n\tif !ok {\n\t\treturn fmt.Errorf(\"expecting a TreeNode but got %T\", ctx.Value(KeyParent))\n\t}\n\n\troot := NewTreeNode(client.SvcGVR, client.FQN(svc.Namespace, svc.Name))\n\too, err := s.locatePods(ctx, svc.Namespace, svc.Spec.Selector)\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx = context.WithValue(ctx, KeyParent, root)\n\tvar re Pod\n\tfor _, o := range oo {\n\t\tp, ok := o.(*unstructured.Unstructured)\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"expecting *Unstructured but got %T\", o)\n\t\t}\n\t\tif err := re.Render(ctx, ns, &render.PodWithMetrics{Raw: p}); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\troot.Extras[StatusKey] = OkStatus\n\n\tif root.IsLeaf() {\n\t\treturn nil\n\t}\n\tgvr, nsID := client.NsGVR, client.FQN(client.ClusterScope, svc.Namespace)\n\tnsn := parent.Find(gvr, nsID)\n\tif nsn == nil {\n\t\tnsn = NewTreeNode(gvr, nsID)\n\t\tparent.Add(nsn)\n\t}\n\tnsn.Add(root)\n\n\treturn nil\n}\n\nfunc (s *Service) locatePods(ctx context.Context, ns string, sel map[string]string) ([]runtime.Object, error) {\n\tf, ok := ctx.Value(internal.KeyFactory).(dao.Factory)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"expecting a factory but got %T\", ctx.Value(internal.KeyFactory))\n\t}\n\n\tll := make([]string, 0, len(sel))\n\tfor k, v := range sel {\n\t\tll = append(ll, fmt.Sprintf(\"%s=%s\", k, v))\n\t}\n\n\tfsel, err := labels.ConvertSelectorToLabelsMap(strings.Join(ll, \",\"))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn f.List(client.PodGVR, ns, false, fsel.AsSelector())\n}\n"
  },
  {
    "path": "internal/xray/svc_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray_test\n\nimport (\n\t\"context\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal\"\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/xray\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n\t\"k8s.io/apimachinery/pkg/runtime\"\n)\n\nfunc TestServiceRender(t *testing.T) {\n\tuu := map[string]struct {\n\t\tfile           string\n\t\tlevel1, level2 int\n\t\tstatus         string\n\t}{\n\t\t\"plain\": {\n\t\t\tfile:   \"svc\",\n\t\t\tlevel1: 1,\n\t\t\tlevel2: 1,\n\t\t\tstatus: xray.OkStatus,\n\t\t},\n\t}\n\n\tvar re xray.Service\n\tfor k := range uu {\n\t\tf := makeFactory()\n\t\tf.rows = map[*client.GVR][]runtime.Object{client.PodGVR: {load(t, \"po\")}}\n\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\to := load(t, u.file)\n\t\t\troot := xray.NewTreeNode(client.SvcGVR, \"services\")\n\t\t\tctx := context.WithValue(context.Background(), xray.KeyParent, root)\n\t\t\tctx = context.WithValue(ctx, internal.KeyFactory, f)\n\n\t\t\trequire.NoError(t, re.Render(ctx, \"\", o))\n\t\t\tassert.Equal(t, u.level1, root.CountChildren())\n\t\t\tassert.Equal(t, u.level2, root.Children[0].CountChildren())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/xray/testdata/cilium.json",
    "content": "{\n    \"apiVersion\": \"v1\",\n    \"kind\": \"Pod\",\n    \"metadata\": {\n        \"creationTimestamp\": \"2020-01-21T00:06:31Z\",\n        \"generateName\": \"cilium-operator-55658fb5c4-\",\n        \"labels\": {\n            \"io.cilium/app\": \"operator\",\n            \"name\": \"cilium-operator\",\n            \"pod-template-hash\": \"55658fb5c4\"\n        },\n        \"name\": \"cilium-operator-55658fb5c4-rxtnl\",\n        \"namespace\": \"kube-system\",\n        \"ownerReferences\": [\n            {\n                \"apiVersion\": \"apps/v1\",\n                \"blockOwnerDeletion\": true,\n                \"controller\": true,\n                \"kind\": \"ReplicaSet\",\n                \"name\": \"cilium-operator-55658fb5c4\",\n                \"uid\": \"aa49a24b-e5b7-4349-88ce-c275ee36097c\"\n            }\n        ],\n        \"resourceVersion\": \"1255\",\n        \"selfLink\": \"/api/v1/namespaces/kube-system/pods/cilium-operator-55658fb5c4-rxtnl\",\n        \"uid\": \"db060299-45c3-40c6-9a87-d8643f0d51e2\"\n    },\n    \"spec\": {\n        \"containers\": [\n            {\n                \"args\": [\n                    \"--debug=$(CILIUM_DEBUG)\",\n                    \"--identity-allocation-mode=$(CILIUM_IDENTITY_ALLOCATION_MODE)\"\n                ],\n                \"command\": [\n                    \"cilium-operator\"\n                ],\n                \"env\": [\n                    {\n                        \"name\": \"CILIUM_K8S_NAMESPACE\",\n                        \"valueFrom\": {\n                            \"fieldRef\": {\n                                \"apiVersion\": \"v1\",\n                                \"fieldPath\": \"metadata.namespace\"\n                            }\n                        }\n                    },\n                    {\n                        \"name\": \"K8S_NODE_NAME\",\n                        \"valueFrom\": {\n                            \"fieldRef\": {\n                                \"apiVersion\": \"v1\",\n                                \"fieldPath\": \"spec.nodeName\"\n                            }\n                        }\n                    },\n                    {\n                        \"name\": \"CILIUM_DEBUG\",\n                        \"valueFrom\": {\n                            \"configMapKeyRef\": {\n                                \"key\": \"debug\",\n                                \"name\": \"cilium-config\",\n                                \"optional\": true\n                            }\n                        }\n                    },\n                    {\n                        \"name\": \"CILIUM_CLUSTER_NAME\",\n                        \"valueFrom\": {\n                            \"configMapKeyRef\": {\n                                \"key\": \"cluster-name\",\n                                \"name\": \"cilium-config\",\n                                \"optional\": true\n                            }\n                        }\n                    },\n                    {\n                        \"name\": \"CILIUM_CLUSTER_ID\",\n                        \"valueFrom\": {\n                            \"configMapKeyRef\": {\n                                \"key\": \"cluster-id\",\n                                \"name\": \"cilium-config\",\n                                \"optional\": true\n                            }\n                        }\n                    },\n                    {\n                        \"name\": \"CILIUM_IPAM\",\n                        \"valueFrom\": {\n                            \"configMapKeyRef\": {\n                                \"key\": \"ipam\",\n                                \"name\": \"cilium-config\",\n                                \"optional\": true\n                            }\n                        }\n                    },\n                    {\n                        \"name\": \"CILIUM_DISABLE_ENDPOINT_CRD\",\n                        \"valueFrom\": {\n                            \"configMapKeyRef\": {\n                                \"key\": \"disable-endpoint-crd\",\n                                \"name\": \"cilium-config\",\n                                \"optional\": true\n                            }\n                        }\n                    },\n                    {\n                        \"name\": \"CILIUM_KVSTORE\",\n                        \"valueFrom\": {\n                            \"configMapKeyRef\": {\n                                \"key\": \"kvstore\",\n                                \"name\": \"cilium-config\",\n                                \"optional\": true\n                            }\n                        }\n                    },\n                    {\n                        \"name\": \"CILIUM_KVSTORE_OPT\",\n                        \"valueFrom\": {\n                            \"configMapKeyRef\": {\n                                \"key\": \"kvstore-opt\",\n                                \"name\": \"cilium-config\",\n                                \"optional\": true\n                            }\n                        }\n                    },\n                    {\n                        \"name\": \"AWS_ACCESS_KEY_ID\",\n                        \"valueFrom\": {\n                            \"secretKeyRef\": {\n                                \"key\": \"AWS_ACCESS_KEY_ID\",\n                                \"name\": \"cilium-aws\",\n                                \"optional\": true\n                            }\n                        }\n                    },\n                    {\n                        \"name\": \"AWS_SECRET_ACCESS_KEY\",\n                        \"valueFrom\": {\n                            \"secretKeyRef\": {\n                                \"key\": \"AWS_SECRET_ACCESS_KEY\",\n                                \"name\": \"cilium-aws\",\n                                \"optional\": true\n                            }\n                        }\n                    },\n                    {\n                        \"name\": \"AWS_DEFAULT_REGION\",\n                        \"valueFrom\": {\n                            \"secretKeyRef\": {\n                                \"key\": \"AWS_DEFAULT_REGION\",\n                                \"name\": \"cilium-aws\",\n                                \"optional\": true\n                            }\n                        }\n                    },\n                    {\n                        \"name\": \"CILIUM_IDENTITY_ALLOCATION_MODE\",\n                        \"valueFrom\": {\n                            \"configMapKeyRef\": {\n                                \"key\": \"identity-allocation-mode\",\n                                \"name\": \"cilium-config\",\n                                \"optional\": true\n                            }\n                        }\n                    }\n                ],\n                \"image\": \"docker.io/cilium/operator:v1.6.5\",\n                \"imagePullPolicy\": \"IfNotPresent\",\n                \"livenessProbe\": {\n                    \"failureThreshold\": 3,\n                    \"httpGet\": {\n                        \"path\": \"/healthz\",\n                        \"port\": 9234,\n                        \"scheme\": \"HTTP\"\n                    },\n                    \"initialDelaySeconds\": 60,\n                    \"periodSeconds\": 10,\n                    \"successThreshold\": 1,\n                    \"timeoutSeconds\": 3\n                },\n                \"name\": \"cilium-operator\",\n                \"resources\": {},\n                \"terminationMessagePath\": \"/dev/termination-log\",\n                \"terminationMessagePolicy\": \"File\",\n                \"volumeMounts\": [\n                    {\n                        \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\",\n                        \"name\": \"cilium-operator-token-r9t5t\",\n                        \"readOnly\": true\n                    }\n                ]\n            }\n        ],\n        \"dnsPolicy\": \"ClusterFirst\",\n        \"enableServiceLinks\": true,\n        \"hostNetwork\": true,\n        \"nodeName\": \"minikube\",\n        \"priority\": 0,\n        \"restartPolicy\": \"Always\",\n        \"schedulerName\": \"default-scheduler\",\n        \"securityContext\": {},\n        \"serviceAccount\": \"cilium-operator\",\n        \"serviceAccountName\": \"cilium-operator\",\n        \"terminationGracePeriodSeconds\": 30,\n        \"tolerations\": [\n            {\n                \"effect\": \"NoExecute\",\n                \"key\": \"node.kubernetes.io/not-ready\",\n                \"operator\": \"Exists\",\n                \"tolerationSeconds\": 300\n            },\n            {\n                \"effect\": \"NoExecute\",\n                \"key\": \"node.kubernetes.io/unreachable\",\n                \"operator\": \"Exists\",\n                \"tolerationSeconds\": 300\n            }\n        ],\n        \"volumes\": [\n            {\n                \"name\": \"cilium-operator-token-r9t5t\",\n                \"secret\": {\n                    \"defaultMode\": 420,\n                    \"secretName\": \"cilium-operator-token-r9t5t\"\n                }\n            }\n        ]\n    },\n    \"status\": {\n        \"conditions\": [\n            {\n                \"lastProbeTime\": null,\n                \"lastTransitionTime\": \"2020-01-21T00:07:39Z\",\n                \"status\": \"True\",\n                \"type\": \"Initialized\"\n            },\n            {\n                \"lastProbeTime\": null,\n                \"lastTransitionTime\": \"2020-01-21T00:07:47Z\",\n                \"status\": \"True\",\n                \"type\": \"Ready\"\n            },\n            {\n                \"lastProbeTime\": null,\n                \"lastTransitionTime\": \"2020-01-21T00:07:47Z\",\n                \"status\": \"True\",\n                \"type\": \"ContainersReady\"\n            },\n            {\n                \"lastProbeTime\": null,\n                \"lastTransitionTime\": \"2020-01-21T00:07:39Z\",\n                \"status\": \"True\",\n                \"type\": \"PodScheduled\"\n            }\n        ],\n        \"containerStatuses\": [\n            {\n                \"containerID\": \"docker://9bc42a0d9395adafd9f8d922350c9029f8fa234060df9b03dd5e256804613f68\",\n                \"image\": \"cilium/operator:v1.6.5\",\n                \"imageID\": \"docker-pullable://cilium/operator@sha256:bcf273e7af15e7a0c9eb8df2f87fc81fe56323217ec8b2b35cd9cd5115920055\",\n                \"lastState\": {},\n                \"name\": \"cilium-operator\",\n                \"ready\": true,\n                \"restartCount\": 0,\n                \"started\": true,\n                \"state\": {\n                    \"running\": {\n                        \"startedAt\": \"2020-01-21T00:07:46Z\"\n                    }\n                }\n            }\n        ],\n        \"hostIP\": \"192.168.64.7\",\n        \"phase\": \"Running\",\n        \"podIP\": \"192.168.64.7\",\n        \"podIPs\": [\n            {\n                \"ip\": \"192.168.64.7\"\n            }\n        ],\n        \"qosClass\": \"BestEffort\",\n        \"startTime\": \"2020-01-21T00:07:39Z\"\n    }\n}\n"
  },
  {
    "path": "internal/xray/testdata/dp.json",
    "content": "{\n    \"apiVersion\": \"apps/v1\",\n    \"kind\": \"Deployment\",\n    \"metadata\": {\n        \"annotations\": {\n            \"deployment.kubernetes.io/revision\": \"3\",\n            \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"apps/v1\\\",\\\"kind\\\":\\\"Deployment\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"labels\\\":{\\\"app\\\":\\\"nginx\\\"},\\\"name\\\":\\\"nginx\\\",\\\"namespace\\\":\\\"default\\\"},\\\"spec\\\":{\\\"replicas\\\":1,\\\"selector\\\":{\\\"matchLabels\\\":{\\\"app\\\":\\\"nginx\\\"}},\\\"template\\\":{\\\"metadata\\\":{\\\"labels\\\":{\\\"app\\\":\\\"nginx\\\"}},\\\"spec\\\":{\\\"containers\\\":[{\\\"env\\\":[{\\\"name\\\":\\\"FRED\\\",\\\"valueFrom\\\":{\\\"configMapKeyRef\\\":{\\\"key\\\":\\\"fred\\\",\\\"name\\\":\\\"busy\\\"}}},{\\\"name\\\":\\\"PROPS\\\",\\\"valueFrom\\\":{\\\"configMapKeyRef\\\":{\\\"key\\\":\\\"props\\\",\\\"name\\\":\\\"busy\\\"}}}],\\\"image\\\":\\\"k8s.gcr.io/nginx-slim:0.8\\\",\\\"name\\\":\\\"nginx\\\",\\\"ports\\\":[{\\\"containerPort\\\":80}],\\\"resources\\\":{\\\"limits\\\":{\\\"cpu\\\":\\\"100m\\\",\\\"memory\\\":\\\"200Mi\\\"}}}]}}}}\\n\"\n        },\n        \"creationTimestamp\": \"2020-01-16T04:18:04Z\",\n        \"generation\": 4,\n        \"labels\": {\n            \"app\": \"nginx\"\n        },\n        \"name\": \"nginx\",\n        \"namespace\": \"default\",\n        \"resourceVersion\": \"3338230\",\n        \"selfLink\": \"/apis/apps/v1/namespaces/default/deployments/nginx\",\n        \"uid\": \"a2baf77e-5301-4efd-ac40-ff3da9716c80\"\n    },\n    \"spec\": {\n        \"progressDeadlineSeconds\": 600,\n        \"replicas\": 1,\n        \"revisionHistoryLimit\": 10,\n        \"selector\": {\n            \"matchLabels\": {\n                \"app\": \"nginx\"\n            }\n        },\n        \"strategy\": {\n            \"rollingUpdate\": {\n                \"maxSurge\": \"25%\",\n                \"maxUnavailable\": \"25%\"\n            },\n            \"type\": \"RollingUpdate\"\n        },\n        \"template\": {\n            \"metadata\": {\n                \"creationTimestamp\": null,\n                \"labels\": {\n                    \"app\": \"nginx\"\n                }\n            },\n            \"spec\": {\n                \"containers\": [\n                    {\n                        \"env\": [\n                            {\n                                \"name\": \"FRED\",\n                                \"valueFrom\": {\n                                    \"configMapKeyRef\": {\n                                        \"key\": \"fred\",\n                                        \"name\": \"busy\"\n                                    }\n                                }\n                            },\n                            {\n                                \"name\": \"PROPS\",\n                                \"valueFrom\": {\n                                    \"configMapKeyRef\": {\n                                        \"key\": \"props\",\n                                        \"name\": \"busy\"\n                                    }\n                                }\n                            }\n                        ],\n                        \"image\": \"k8s.gcr.io/nginx-slim:0.8\",\n                        \"imagePullPolicy\": \"IfNotPresent\",\n                        \"name\": \"nginx\",\n                        \"ports\": [\n                            {\n                                \"containerPort\": 80,\n                                \"protocol\": \"TCP\"\n                            }\n                        ],\n                        \"resources\": {\n                            \"limits\": {\n                                \"cpu\": \"100m\",\n                                \"memory\": \"200Mi\"\n                            }\n                        },\n                        \"terminationMessagePath\": \"/dev/termination-log\",\n                        \"terminationMessagePolicy\": \"File\"\n                    }\n                ],\n                \"dnsPolicy\": \"ClusterFirst\",\n                \"restartPolicy\": \"Always\",\n                \"schedulerName\": \"default-scheduler\",\n                \"securityContext\": {},\n                \"terminationGracePeriodSeconds\": 30\n            }\n        }\n    },\n    \"status\": {\n        \"availableReplicas\": 1,\n        \"conditions\": [\n            {\n                \"lastTransitionTime\": \"2020-01-16T14:52:45Z\",\n                \"lastUpdateTime\": \"2020-01-16T14:52:45Z\",\n                \"message\": \"Deployment has minimum availability.\",\n                \"reason\": \"MinimumReplicasAvailable\",\n                \"status\": \"True\",\n                \"type\": \"Available\"\n            },\n            {\n                \"lastTransitionTime\": \"2020-01-18T01:20:50Z\",\n                \"lastUpdateTime\": \"2020-01-18T01:20:50Z\",\n                \"message\": \"ReplicaSet \\\"nginx-5bbc876d89\\\" has successfully progressed.\",\n                \"reason\": \"NewReplicaSetAvailable\",\n                \"status\": \"True\",\n                \"type\": \"Progressing\"\n            }\n        ],\n        \"observedGeneration\": 4,\n        \"readyReplicas\": 1,\n        \"replicas\": 1,\n        \"updatedReplicas\": 1\n    }\n}\n"
  },
  {
    "path": "internal/xray/testdata/ds.json",
    "content": "{\n    \"apiVersion\": \"apps/v1\",\n    \"kind\": \"DaemonSet\",\n    \"metadata\": {\n        \"annotations\": {\n            \"deprecated.daemonset.template.generation\": \"1\",\n            \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"apps/v1\\\",\\\"kind\\\":\\\"DaemonSet\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"labels\\\":{\\\"k8s-app\\\":\\\"fluentd-logging\\\"},\\\"name\\\":\\\"fluentd-elasticsearch\\\",\\\"namespace\\\":\\\"default\\\"},\\\"spec\\\":{\\\"selector\\\":{\\\"matchLabels\\\":{\\\"name\\\":\\\"fluentd-elasticsearch\\\"}},\\\"template\\\":{\\\"metadata\\\":{\\\"labels\\\":{\\\"name\\\":\\\"fluentd-elasticsearch\\\"}},\\\"spec\\\":{\\\"containers\\\":[{\\\"image\\\":\\\"fluentd\\\",\\\"name\\\":\\\"fluentd-elasticsearch\\\",\\\"resources\\\":{\\\"limits\\\":{\\\"memory\\\":\\\"200Mi\\\"},\\\"requests\\\":{\\\"cpu\\\":\\\"100m\\\",\\\"memory\\\":\\\"200Mi\\\"}},\\\"volumeMounts\\\":[{\\\"mountPath\\\":\\\"/var/log\\\",\\\"name\\\":\\\"varlog\\\"},{\\\"mountPath\\\":\\\"/var/lib/docker/containers\\\",\\\"name\\\":\\\"varlibdockercontainers\\\",\\\"readOnly\\\":true}]}],\\\"terminationGracePeriodSeconds\\\":1,\\\"tolerations\\\":[{\\\"effect\\\":\\\"NoSchedule\\\",\\\"key\\\":\\\"node-role.kubernetes.io/master\\\"}],\\\"volumes\\\":[{\\\"hostPath\\\":{\\\"path\\\":\\\"/var/log\\\"},\\\"name\\\":\\\"varlog\\\"},{\\\"hostPath\\\":{\\\"path\\\":\\\"/var/lib/docker/containers\\\"},\\\"name\\\":\\\"varlibdockercontainers\\\"}]}}}}\\n\"\n        },\n        \"creationTimestamp\": \"2020-01-18T14:43:04Z\",\n        \"generation\": 1,\n        \"labels\": {\n            \"k8s-app\": \"fluentd-logging\"\n        },\n        \"name\": \"fluentd-elasticsearch\",\n        \"namespace\": \"default\",\n        \"resourceVersion\": \"3450170\",\n        \"selfLink\": \"/apis/apps/v1/namespaces/default/daemonsets/fluentd-elasticsearch\",\n        \"uid\": \"8c03864a-a428-4769-b89c-11d66e01614d\"\n    },\n    \"spec\": {\n        \"revisionHistoryLimit\": 10,\n        \"selector\": {\n            \"matchLabels\": {\n                \"name\": \"fluentd-elasticsearch\"\n            }\n        },\n        \"template\": {\n            \"metadata\": {\n                \"creationTimestamp\": null,\n                \"labels\": {\n                    \"name\": \"fluentd-elasticsearch\"\n                }\n            },\n            \"spec\": {\n                \"containers\": [\n                    {\n                        \"image\": \"fluentd\",\n                        \"imagePullPolicy\": \"Always\",\n                        \"name\": \"fluentd-elasticsearch\",\n                        \"resources\": {\n                            \"limits\": {\n                                \"memory\": \"200Mi\"\n                            },\n                            \"requests\": {\n                                \"cpu\": \"100m\",\n                                \"memory\": \"200Mi\"\n                            }\n                        },\n                        \"terminationMessagePath\": \"/dev/termination-log\",\n                        \"terminationMessagePolicy\": \"File\",\n                        \"volumeMounts\": [\n                            {\n                                \"mountPath\": \"/var/log\",\n                                \"name\": \"varlog\"\n                            },\n                            {\n                                \"mountPath\": \"/var/lib/docker/containers\",\n                                \"name\": \"varlibdockercontainers\",\n                                \"readOnly\": true\n                            }\n                        ]\n                    }\n                ],\n                \"dnsPolicy\": \"ClusterFirst\",\n                \"restartPolicy\": \"Always\",\n                \"schedulerName\": \"default-scheduler\",\n                \"securityContext\": {},\n                \"terminationGracePeriodSeconds\": 1,\n                \"tolerations\": [\n                    {\n                        \"effect\": \"NoSchedule\",\n                        \"key\": \"node-role.kubernetes.io/master\"\n                    }\n                ],\n                \"volumes\": [\n                    {\n                        \"hostPath\": {\n                            \"path\": \"/var/log\",\n                            \"type\": \"\"\n                        },\n                        \"name\": \"varlog\"\n                    },\n                    {\n                        \"hostPath\": {\n                            \"path\": \"/var/lib/docker/containers\",\n                            \"type\": \"\"\n                        },\n                        \"name\": \"varlibdockercontainers\"\n                    }\n                ]\n            }\n        },\n        \"updateStrategy\": {\n            \"rollingUpdate\": {\n                \"maxUnavailable\": 1\n            },\n            \"type\": \"RollingUpdate\"\n        }\n    },\n    \"status\": {\n        \"currentNumberScheduled\": 1,\n        \"desiredNumberScheduled\": 1,\n        \"numberAvailable\": 1,\n        \"numberMisscheduled\": 0,\n        \"numberReady\": 1,\n        \"observedGeneration\": 1,\n        \"updatedNumberScheduled\": 1\n    }\n}\n"
  },
  {
    "path": "internal/xray/testdata/init.json",
    "content": "{\n  \"apiVersion\": \"v1\",\n  \"kind\": \"Pod\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"v1\\\",\\\"kind\\\":\\\"Pod\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"hurry-up-and-wait\\\",\\\"namespace\\\":\\\"default\\\"},\\\"spec\\\":{\\\"containers\\\":[{\\\"command\\\":[\\\"sh\\\",\\\"-c\\\",\\\"echo The app is running! \\\\u0026\\\\u0026 sleep 3600\\\"],\\\"image\\\":\\\"busybox\\\",\\\"name\\\":\\\"busy\\\",\\\"resources\\\":{\\\"limits\\\":{\\\"cpu\\\":\\\"100m\\\",\\\"memory\\\":\\\"100Mi\\\"}}}],\\\"initContainers\\\":[{\\\"command\\\":[\\\"sh\\\",\\\"-c\\\",\\\"echo \\\\\\\"sleeping...\\\\\\\"; sleep 10\\\"],\\\"image\\\":\\\"busybox\\\",\\\"name\\\":\\\"init-sleep\\\",\\\"resources\\\":{\\\"limits\\\":{\\\"cpu\\\":\\\"100m\\\",\\\"memory\\\":\\\"100Mi\\\"}}}]}}\\n\"\n    },\n    \"creationTimestamp\": \"2020-01-18T06:31:29Z\",\n    \"name\": \"hurry-up-and-wait\",\n    \"namespace\": \"default\",\n    \"resourceVersion\": \"3381576\",\n    \"selfLink\": \"/api/v1/namespaces/default/pods/hurry-up-and-wait\",\n    \"uid\": \"6b29055a-433b-4398-bfde-0fd371759bbf\"\n  },\n  \"spec\": {\n    \"containers\": [\n      {\n        \"command\": [\n          \"sh\",\n          \"-c\",\n          \"echo The app is running! \\u0026\\u0026 sleep 3600\"\n        ],\n        \"image\": \"busybox\",\n        \"imagePullPolicy\": \"Always\",\n        \"name\": \"busy\",\n        \"resources\": {\n          \"limits\": {\n            \"cpu\": \"100m\",\n            \"memory\": \"100Mi\"\n          },\n          \"requests\": {\n            \"cpu\": \"100m\",\n            \"memory\": \"100Mi\"\n          }\n        },\n        \"terminationMessagePath\": \"/dev/termination-log\",\n        \"terminationMessagePolicy\": \"File\",\n        \"volumeMounts\": [\n          {\n            \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\",\n            \"name\": \"default-token-rr22g\",\n            \"readOnly\": true\n          }\n        ]\n      }\n    ],\n    \"dnsPolicy\": \"ClusterFirst\",\n    \"enableServiceLinks\": true,\n    \"initContainers\": [\n      {\n        \"command\": [\n          \"sh\",\n          \"-c\",\n          \"echo \\\"sleeping...\\\"; sleep 10\"\n        ],\n        \"image\": \"busybox\",\n        \"imagePullPolicy\": \"Always\",\n        \"name\": \"init-sleep\",\n        \"resources\": {\n          \"limits\": {\n            \"cpu\": \"100m\",\n            \"memory\": \"100Mi\"\n          },\n          \"requests\": {\n            \"cpu\": \"100m\",\n            \"memory\": \"100Mi\"\n          }\n        },\n        \"terminationMessagePath\": \"/dev/termination-log\",\n        \"terminationMessagePolicy\": \"File\",\n        \"volumeMounts\": [\n          {\n            \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\",\n            \"name\": \"default-token-rr22g\",\n            \"readOnly\": true\n          }\n        ]\n      }\n    ],\n    \"nodeName\": \"minikube\",\n    \"priority\": 0,\n    \"restartPolicy\": \"Always\",\n    \"schedulerName\": \"default-scheduler\",\n    \"securityContext\": {},\n    \"serviceAccount\": \"default\",\n    \"serviceAccountName\": \"default\",\n    \"terminationGracePeriodSeconds\": 30,\n    \"tolerations\": [\n      {\n        \"effect\": \"NoExecute\",\n        \"key\": \"node.kubernetes.io/not-ready\",\n        \"operator\": \"Exists\",\n        \"tolerationSeconds\": 300\n      },\n      {\n        \"effect\": \"NoExecute\",\n        \"key\": \"node.kubernetes.io/unreachable\",\n        \"operator\": \"Exists\",\n        \"tolerationSeconds\": 300\n      }\n    ],\n    \"volumes\": [\n      {\n        \"name\": \"default-token-rr22g\",\n        \"secret\": {\n          \"defaultMode\": 420,\n          \"secretName\": \"default-token-rr22g\"\n        }\n      }\n    ]\n  },\n  \"status\": {\n    \"conditions\": [\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2020-01-18T06:31:42Z\",\n        \"status\": \"True\",\n        \"type\": \"Initialized\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2020-01-18T06:31:44Z\",\n        \"status\": \"True\",\n        \"type\": \"Ready\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2020-01-18T06:31:44Z\",\n        \"status\": \"True\",\n        \"type\": \"ContainersReady\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2020-01-18T06:31:29Z\",\n        \"status\": \"True\",\n        \"type\": \"PodScheduled\"\n      }\n    ],\n    \"containerStatuses\": [\n      {\n        \"containerID\": \"docker://3c4de1de5d3c8f78bcce5f65218d5cbe4ed7b7b86261dd74dcc0f96e832e7db3\",\n        \"image\": \"busybox:latest\",\n        \"imageID\": \"docker-pullable://busybox@sha256:6915be4043561d64e0ab0f8f098dc2ac48e077fe23f488ac24b665166898115a\",\n        \"lastState\": {},\n        \"name\": \"busy\",\n        \"ready\": true,\n        \"restartCount\": 0,\n        \"started\": true,\n        \"state\": {\n          \"running\": {\n            \"startedAt\": \"2020-01-18T06:31:43Z\"\n          }\n        }\n      }\n    ],\n    \"hostIP\": \"192.168.64.6\",\n    \"initContainerStatuses\": [\n      {\n        \"containerID\": \"docker://87f5d5f73827b402263ef77ca72b715c4ad858e7da71abc5655cc049e4c2ae20\",\n        \"image\": \"busybox:latest\",\n        \"imageID\": \"docker-pullable://busybox@sha256:6915be4043561d64e0ab0f8f098dc2ac48e077fe23f488ac24b665166898115a\",\n        \"lastState\": {},\n        \"name\": \"init-sleep\",\n        \"ready\": true,\n        \"restartCount\": 0,\n        \"state\": {\n          \"terminated\": {\n            \"containerID\": \"docker://87f5d5f73827b402263ef77ca72b715c4ad858e7da71abc5655cc049e4c2ae20\",\n            \"exitCode\": 0,\n            \"finishedAt\": \"2020-01-18T06:31:42Z\",\n            \"reason\": \"Completed\",\n            \"startedAt\": \"2020-01-18T06:31:32Z\"\n          }\n        }\n      }\n    ],\n    \"phase\": \"Running\",\n    \"podIP\": \"172.17.0.11\",\n    \"podIPs\": [\n      {\n        \"ip\": \"172.17.0.11\"\n      }\n    ],\n    \"qosClass\": \"Guaranteed\",\n    \"startTime\": \"2020-01-18T06:31:29Z\"\n  }\n}"
  },
  {
    "path": "internal/xray/testdata/ns.json",
    "content": "{\n    \"apiVersion\": \"v1\",\n    \"kind\": \"Namespace\",\n    \"metadata\": {\n        \"creationTimestamp\": \"2019-12-31T20:49:23Z\",\n        \"name\": \"default\",\n        \"resourceVersion\": \"146\",\n        \"selfLink\": \"/api/v1/namespaces/default\",\n        \"uid\": \"3da8811c-7632-4a42-b4f5-608c21165ff7\"\n    },\n    \"spec\": {\n        \"finalizers\": [\n            \"kubernetes\"\n        ]\n    },\n    \"status\": {\n        \"phase\": \"Active\"\n    }\n}\n"
  },
  {
    "path": "internal/xray/testdata/po.json",
    "content": "{\n  \"apiVersion\": \"v1\",\n  \"kind\": \"Pod\",\n  \"metadata\": {\n    \"annotations\": {\n      \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"v1\\\",\\\"kind\\\":\\\"Pod\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"nginx\\\",\\\"namespace\\\":\\\"default\\\"},\\\"spec\\\":{\\\"containers\\\":[{\\\"image\\\":\\\"nginx:alpine\\\",\\\"name\\\":\\\"nginx\\\",\\\"ports\\\":[{\\\"containerPort\\\":80}],\\\"volumeMounts\\\":[{\\\"mountPath\\\":\\\"/usr/share/nginx/html\\\",\\\"name\\\":\\\"index\\\"}]}],\\\"terminationGracePeriodSeconds\\\":0,\\\"volumes\\\":[{\\\"name\\\":\\\"index\\\",\\\"persistentVolumeClaim\\\":{\\\"claimName\\\":\\\"web\\\"}}]}}\\n\"\n    },\n    \"creationTimestamp\": \"2019-08-09T05:12:19Z\",\n    \"name\": \"nginx\",\n    \"namespace\": \"default\",\n    \"resourceVersion\": \"1482816\",\n    \"selfLink\": \"/api/v1/namespaces/default/pods/nginx\",\n    \"uid\": \"614908ed-415b-4506-8370-e3e36fa8cc13\"\n  },\n  \"spec\": {\n    \"containers\": [\n      {\n        \"image\": \"nginx:alpine\",\n        \"imagePullPolicy\": \"IfNotPresent\",\n        \"name\": \"nginx\",\n        \"ports\": [\n          {\n            \"containerPort\": 80,\n            \"protocol\": \"TCP\"\n          }\n        ],\n        \"resources\": {\n          \"limits\": {\n            \"memory\": \"170Mi\"\n          },\n          \"requests\": {\n            \"cpu\": \"100m\",\n            \"memory\": \"70Mi\"\n          }\n        },\n        \"terminationMessagePath\": \"/dev/termination-log\",\n        \"terminationMessagePolicy\": \"File\",\n        \"volumeMounts\": [\n          {\n            \"mountPath\": \"/usr/share/nginx/html\",\n            \"name\": \"index\"\n          },\n          {\n            \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\",\n            \"name\": \"default-token-9ph8s\",\n            \"readOnly\": true\n          }\n        ]\n      }\n    ],\n    \"dnsPolicy\": \"ClusterFirst\",\n    \"enableServiceLinks\": true,\n    \"nodeName\": \"minikube\",\n    \"priority\": 0,\n    \"restartPolicy\": \"Always\",\n    \"schedulerName\": \"default-scheduler\",\n    \"securityContext\": {},\n    \"serviceAccount\": \"default\",\n    \"serviceAccountName\": \"default\",\n    \"terminationGracePeriodSeconds\": 0,\n    \"tolerations\": [\n      {\n        \"effect\": \"NoExecute\",\n        \"key\": \"node.kubernetes.io/not-ready\",\n        \"operator\": \"Exists\",\n        \"tolerationSeconds\": 300\n      },\n      {\n        \"effect\": \"NoExecute\",\n        \"key\": \"node.kubernetes.io/unreachable\",\n        \"operator\": \"Exists\",\n        \"tolerationSeconds\": 300\n      }\n    ],\n    \"volumes\": [\n      {\n        \"name\": \"index\",\n        \"persistentVolumeClaim\": {\n          \"claimName\": \"web\"\n        }\n      },\n      {\n        \"name\": \"default-token-9ph8s\",\n        \"secret\": {\n          \"defaultMode\": 420,\n          \"secretName\": \"default-token-9ph8s\"\n        }\n      }\n    ]\n  },\n  \"status\": {\n    \"conditions\": [\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-08-09T05:12:19Z\",\n        \"status\": \"True\",\n        \"type\": \"Initialized\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-08-09T05:12:21Z\",\n        \"status\": \"True\",\n        \"type\": \"Ready\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-08-09T05:12:21Z\",\n        \"status\": \"True\",\n        \"type\": \"ContainersReady\"\n      },\n      {\n        \"lastProbeTime\": null,\n        \"lastTransitionTime\": \"2019-08-09T05:12:19Z\",\n        \"status\": \"True\",\n        \"type\": \"PodScheduled\"\n      }\n    ],\n    \"containerStatuses\": [\n      {\n        \"containerID\": \"docker://421bd26d6c682f14b5ea1dcaf06e14a509b2b702fc7793e820520eb1e28e2eaf\",\n        \"image\": \"nginx:alpine\",\n        \"imageID\": \"docker-pullable://nginx@sha256:482ead44b2203fa32b3390abdaf97cbdc8ad15c07fb03a3e68d7c35a19ad7595\",\n        \"lastState\": {},\n        \"name\": \"nginx\",\n        \"ready\": true,\n        \"restartCount\": 0,\n        \"state\": {\n          \"running\": {\n            \"startedAt\": \"2019-08-09T05:12:20Z\"\n          }\n        }\n      }\n    ],\n    \"hostIP\": \"192.168.64.104\",\n    \"phase\": \"Running\",\n    \"podIP\": \"172.17.0.6\",\n    \"qosClass\": \"BestEffort\",\n    \"startTime\": \"2019-08-09T05:12:19Z\"\n  }\n}"
  },
  {
    "path": "internal/xray/testdata/rs.json",
    "content": "{\n    \"apiVersion\": \"apps/v1\",\n    \"kind\": \"ReplicaSet\",\n    \"metadata\": {\n        \"annotations\": {\n            \"deployment.kubernetes.io/desired-replicas\": \"1\",\n            \"deployment.kubernetes.io/max-replicas\": \"2\",\n            \"deployment.kubernetes.io/revision\": \"2\"\n        },\n        \"creationTimestamp\": \"2020-01-20T01:34:11Z\",\n        \"generation\": 1,\n        \"labels\": {\n            \"app\": \"nginx-pv\",\n            \"pod-template-hash\": \"6476d7d5c8\"\n        },\n        \"name\": \"nginx-pv-6476d7d5c8\",\n        \"namespace\": \"default\",\n        \"ownerReferences\": [\n            {\n                \"apiVersion\": \"apps/v1\",\n                \"blockOwnerDeletion\": true,\n                \"controller\": true,\n                \"kind\": \"Deployment\",\n                \"name\": \"nginx-pv\",\n                \"uid\": \"68aa70ff-ff7c-4a67-8d4f-fc31ef27ec35\"\n            }\n        ],\n        \"resourceVersion\": \"3743997\",\n        \"selfLink\": \"/apis/apps/v1/namespaces/default/replicasets/nginx-pv-6476d7d5c8\",\n        \"uid\": \"547a036d-94d9-4818-bd9e-ec2939019471\"\n    },\n    \"spec\": {\n        \"replicas\": 1,\n        \"selector\": {\n            \"matchLabels\": {\n                \"app\": \"nginx-pv\",\n                \"pod-template-hash\": \"6476d7d5c8\"\n            }\n        },\n        \"template\": {\n            \"metadata\": {\n                \"creationTimestamp\": null,\n                \"labels\": {\n                    \"app\": \"nginx-pv\",\n                    \"pod-template-hash\": \"6476d7d5c8\"\n                }\n            },\n            \"spec\": {\n                \"automountServiceAccountToken\": true,\n                \"containers\": [\n                    {\n                        \"env\": [\n                            {\n                                \"name\": \"FRED\",\n                                \"valueFrom\": {\n                                    \"configMapKeyRef\": {\n                                        \"key\": \"fred\",\n                                        \"name\": \"busy\"\n                                    }\n                                }\n                            },\n                            {\n                                \"name\": \"PROPS\",\n                                \"valueFrom\": {\n                                    \"configMapKeyRef\": {\n                                        \"key\": \"props\",\n                                        \"name\": \"busy\"\n                                    }\n                                }\n                            }\n                        ],\n                        \"image\": \"k8s.gcr.io/nginx-slim:0.8\",\n                        \"imagePullPolicy\": \"IfNotPresent\",\n                        \"name\": \"nginx\",\n                        \"ports\": [\n                            {\n                                \"containerPort\": 80,\n                                \"protocol\": \"TCP\"\n                            }\n                        ],\n                        \"resources\": {\n                            \"limits\": {\n                                \"cpu\": \"100m\",\n                                \"memory\": \"200Mi\"\n                            }\n                        },\n                        \"terminationMessagePath\": \"/dev/termination-log\",\n                        \"terminationMessagePolicy\": \"File\",\n                        \"volumeMounts\": [\n                            {\n                                \"mountPath\": \"/usr/share/nginx/html\",\n                                \"name\": \"index\"\n                            }\n                        ]\n                    }\n                ],\n                \"dnsPolicy\": \"ClusterFirst\",\n                \"restartPolicy\": \"Always\",\n                \"schedulerName\": \"default-scheduler\",\n                \"securityContext\": {},\n                \"serviceAccount\": \"zorg\",\n                \"serviceAccountName\": \"zorg\",\n                \"terminationGracePeriodSeconds\": 30,\n                \"volumes\": [\n                    {\n                        \"name\": \"index\",\n                        \"persistentVolumeClaim\": {\n                            \"claimName\": \"web\"\n                        }\n                    }\n                ]\n            }\n        }\n    },\n    \"status\": {\n        \"availableReplicas\": 1,\n        \"fullyLabeledReplicas\": 1,\n        \"observedGeneration\": 1,\n        \"readyReplicas\": 1,\n        \"replicas\": 1\n    }\n}\n"
  },
  {
    "path": "internal/xray/testdata/sa.json",
    "content": "{\n    \"apiVersion\": \"v1\",\n    \"kind\": \"ServiceAccount\",\n    \"metadata\": {\n        \"annotations\": {\n            \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"v1\\\",\\\"kind\\\":\\\"ServiceAccount\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"zorg\\\",\\\"namespace\\\":\\\"default\\\"},\\\"secrets\\\":[{\\\"name\\\":\\\"zorg\\\"}]}\\n\"\n        },\n        \"creationTimestamp\": \"2020-01-19T16:31:41Z\",\n        \"name\": \"zorg\",\n        \"namespace\": \"default\",\n        \"resourceVersion\": \"3667084\",\n        \"selfLink\": \"/api/v1/namespaces/default/serviceaccounts/zorg\",\n        \"uid\": \"be8959a7-e324-4cfd-88c1-5fd45c028be6\"\n    },\n    \"secrets\": [\n        {\n            \"name\": \"zorg\"\n        },\n        {\n            \"name\": \"zorg-token-rhhzn\"\n        }\n    ]\n}\n"
  },
  {
    "path": "internal/xray/testdata/sts.json",
    "content": "{\n    \"apiVersion\": \"apps/v1\",\n    \"kind\": \"StatefulSet\",\n    \"metadata\": {\n        \"annotations\": {\n            \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"apps/v1\\\",\\\"kind\\\":\\\"StatefulSet\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"labels\\\":{\\\"app\\\":\\\"nginx-sts\\\"},\\\"name\\\":\\\"nginx-sts\\\",\\\"namespace\\\":\\\"default\\\"},\\\"spec\\\":{\\\"replicas\\\":2,\\\"selector\\\":{\\\"matchLabels\\\":{\\\"app\\\":\\\"nginx-sts\\\"}},\\\"serviceName\\\":\\\"nginx-sts\\\",\\\"template\\\":{\\\"metadata\\\":{\\\"labels\\\":{\\\"app\\\":\\\"nginx-sts\\\"}},\\\"spec\\\":{\\\"containers\\\":[{\\\"image\\\":\\\"k8s.gcr.io/nginx-slim:0.8\\\",\\\"name\\\":\\\"nginx\\\",\\\"ports\\\":[{\\\"containerPort\\\":80,\\\"name\\\":\\\"web\\\"}]}]}}}}\\n\"\n        },\n        \"creationTimestamp\": \"2020-01-15T06:48:21Z\",\n        \"generation\": 1,\n        \"labels\": {\n            \"app\": \"nginx-sts\"\n        },\n        \"name\": \"nginx-sts\",\n        \"namespace\": \"default\",\n        \"resourceVersion\": \"2946929\",\n        \"selfLink\": \"/apis/apps/v1/namespaces/default/statefulsets/nginx-sts\",\n        \"uid\": \"59c516cb-9fe4-4d7f-b7f4-479928506423\"\n    },\n    \"spec\": {\n        \"podManagementPolicy\": \"OrderedReady\",\n        \"replicas\": 2,\n        \"revisionHistoryLimit\": 10,\n        \"selector\": {\n            \"matchLabels\": {\n                \"app\": \"nginx-sts\"\n            }\n        },\n        \"serviceName\": \"nginx-sts\",\n        \"template\": {\n            \"metadata\": {\n                \"creationTimestamp\": null,\n                \"labels\": {\n                    \"app\": \"nginx-sts\"\n                }\n            },\n            \"spec\": {\n                \"containers\": [\n                    {\n                        \"image\": \"k8s.gcr.io/nginx-slim:0.8\",\n                        \"imagePullPolicy\": \"IfNotPresent\",\n                        \"name\": \"nginx\",\n                        \"ports\": [\n                            {\n                                \"containerPort\": 80,\n                                \"name\": \"web\",\n                                \"protocol\": \"TCP\"\n                            }\n                        ],\n                        \"resources\": {},\n                        \"terminationMessagePath\": \"/dev/termination-log\",\n                        \"terminationMessagePolicy\": \"File\"\n                    }\n                ],\n                \"dnsPolicy\": \"ClusterFirst\",\n                \"restartPolicy\": \"Always\",\n                \"schedulerName\": \"default-scheduler\",\n                \"securityContext\": {},\n                \"terminationGracePeriodSeconds\": 30\n            }\n        },\n        \"updateStrategy\": {\n            \"rollingUpdate\": {\n                \"partition\": 0\n            },\n            \"type\": \"RollingUpdate\"\n        }\n    },\n    \"status\": {\n        \"collisionCount\": 0,\n        \"currentReplicas\": 2,\n        \"currentRevision\": \"nginx-sts-688d57df8f\",\n        \"observedGeneration\": 1,\n        \"readyReplicas\": 2,\n        \"replicas\": 2,\n        \"updateRevision\": \"nginx-sts-688d57df8f\",\n        \"updatedReplicas\": 2\n    }\n}\n"
  },
  {
    "path": "internal/xray/testdata/svc.json",
    "content": "{\n    \"apiVersion\": \"v1\",\n    \"kind\": \"Service\",\n    \"metadata\": {\n        \"annotations\": {\n            \"kubectl.kubernetes.io/last-applied-configuration\": \"{\\\"apiVersion\\\":\\\"v1\\\",\\\"kind\\\":\\\"Service\\\",\\\"metadata\\\":{\\\"annotations\\\":{},\\\"name\\\":\\\"nginx\\\",\\\"namespace\\\":\\\"default\\\"},\\\"spec\\\":{\\\"ports\\\":[{\\\"nodePort\\\":30805,\\\"port\\\":8080,\\\"protocol\\\":\\\"TCP\\\",\\\"targetPort\\\":80}],\\\"selector\\\":{\\\"app\\\":\\\"nginx\\\"},\\\"type\\\":\\\"NodePort\\\"}}\\n\"\n        },\n        \"creationTimestamp\": \"2020-01-16T04:18:04Z\",\n        \"name\": \"nginx\",\n        \"namespace\": \"default\",\n        \"resourceVersion\": \"3066081\",\n        \"selfLink\": \"/api/v1/namespaces/default/services/nginx\",\n        \"uid\": \"3dc94561-06ce-4e56-8002-7c4679203d5b\"\n    },\n    \"spec\": {\n        \"clusterIP\": \"10.96.10.89\",\n        \"externalTrafficPolicy\": \"Cluster\",\n        \"ports\": [\n            {\n                \"nodePort\": 30805,\n                \"port\": 8080,\n                \"protocol\": \"TCP\",\n                \"targetPort\": 80\n            }\n        ],\n        \"selector\": {\n            \"app\": \"nginx\"\n        },\n        \"sessionAffinity\": \"None\",\n        \"type\": \"NodePort\"\n    },\n    \"status\": {\n        \"loadBalancer\": {}\n    }\n}\n"
  },
  {
    "path": "internal/xray/tree_node.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray\n\nimport (\n\t\"fmt\"\n\t\"log/slog\"\n\t\"reflect\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/dao\"\n\t\"github.com/fvbommel/sortorder\"\n)\n\nconst (\n\t// KeyParent indicates a parent node context key.\n\tKeyParent TreeRef = \"parent\"\n\n\t// KeySAAutomount indicates whether an automount sa token is active or not.\n\tKeySAAutomount TreeRef = \"automount\"\n\n\t// PathSeparator represents a node path separator.\n\tPathSeparator = \"::\"\n\n\t// StatusKey status map key.\n\tStatusKey = \"status\"\n\n\t// InfoKey state map key.\n\tInfoKey = \"info\"\n\n\t// OkStatus stands for all is cool.\n\tOkStatus = \"ok\"\n\n\t// ToastStatus stands for a resource is not up to snuff\n\t// aka not running or incomplete.\n\tToastStatus = \"toast\"\n\n\t// CompletedStatus stands for a completed resource.\n\tCompletedStatus = \"completed\"\n\n\t// MissingRefStatus stands for a non existing resource reference.\n\tMissingRefStatus = \"noref\"\n)\n\n// ----------------------------------------------------------------------------\n\n// TreeRef namespaces tree context values.\ntype TreeRef string\n\n// ----------------------------------------------------------------------------\n\n// NodeSpec represents a node resource specification.\ntype NodeSpec struct {\n\tGVRs            client.GVRs\n\tPaths, Statuses []string\n}\n\n// ParentGVR returns the parent GVR.\nfunc (s NodeSpec) ParentGVR() *client.GVR {\n\tif len(s.GVRs) > 1 {\n\t\treturn s.GVRs[1]\n\t}\n\n\treturn nil\n}\n\n// ParentPath returns the parent path.\nfunc (s NodeSpec) ParentPath() *string {\n\tif len(s.Paths) > 1 {\n\t\treturn &s.Paths[1]\n\t}\n\treturn nil\n}\n\n// GVR returns the current GVR.\nfunc (s NodeSpec) GVR() *client.GVR {\n\treturn s.GVRs[0]\n}\n\n// Path returns the current path.\nfunc (s NodeSpec) Path() string {\n\treturn s.Paths[0]\n}\n\n// Status returns the current status.\nfunc (s NodeSpec) Status() string {\n\treturn s.Statuses[0]\n}\n\n// AsPath returns path hierarchy as string.\nfunc (s NodeSpec) AsPath() string {\n\treturn strings.Join(s.Paths, PathSeparator)\n}\n\n// AsGVR returns a gvr hierarchy as string.\nfunc (s NodeSpec) AsGVR() string {\n\tss := make([]string, 0, len(s.GVRs))\n\tfor _, gvr := range s.GVRs {\n\t\tss = append(ss, gvr.R())\n\t}\n\n\treturn strings.Join(ss, PathSeparator)\n}\n\n// AsStatus returns a status hierarchy as string.\nfunc (s NodeSpec) AsStatus() string {\n\treturn strings.Join(s.Statuses, PathSeparator)\n}\n\n// ----------------------------------------------------------------------------\n\n// ChildNodes represents a collection of children nodes.\ntype ChildNodes []*TreeNode\n\n// Len returns the list size.\nfunc (c ChildNodes) Len() int {\n\treturn len(c)\n}\n\n// Swap swaps list values.\nfunc (c ChildNodes) Swap(i, j int) {\n\tc[i], c[j] = c[j], c[i]\n}\n\n// Less returns true if i < j.\nfunc (c ChildNodes) Less(i, j int) bool {\n\tid1, id2 := c[i].ID, c[j].ID\n\n\treturn sortorder.NaturalLess(id1, id2)\n}\n\n// ----------------------------------------------------------------------------\n\n// TreeNode represents a resource tree node.\ntype TreeNode struct {\n\tGVR      *client.GVR\n\tID       string\n\tChildren ChildNodes\n\tParent   *TreeNode\n\tExtras   map[string]string\n}\n\n// NewTreeNode returns a new instance.\nfunc NewTreeNode(gvr *client.GVR, id string) *TreeNode {\n\treturn &TreeNode{\n\t\tGVR:    gvr,\n\t\tID:     id,\n\t\tExtras: map[string]string{StatusKey: OkStatus},\n\t}\n}\n\n// CountChildren returns the children count.\nfunc (t *TreeNode) CountChildren() int {\n\treturn len(t.Children)\n}\n\n// Count all the nodes from this node.\nfunc (t *TreeNode) Count(gvr *client.GVR) int {\n\tcounter := 0\n\tif t.GVR == gvr || gvr == client.NoGVR {\n\t\tcounter++\n\t}\n\tfor _, c := range t.Children {\n\t\tcounter += c.Count(gvr)\n\t}\n\treturn counter\n}\n\n// Diff computes a tree diff.\nfunc (t *TreeNode) Diff(d *TreeNode) bool {\n\tif t == nil {\n\t\treturn d != nil\n\t}\n\n\tif t.CountChildren() != d.CountChildren() {\n\t\treturn true\n\t}\n\n\tif t.ID != d.ID || t.GVR != d.GVR || !reflect.DeepEqual(t.Extras, d.Extras) {\n\t\treturn true\n\t}\n\tfor i := 0; i < len(t.Children); i++ {\n\t\tif t.Children[i].Diff(d.Children[i]) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\n// Sort sorts the tree nodes.\nfunc (t *TreeNode) Sort() {\n\tsort.Sort(t.Children)\n\tfor _, c := range t.Children {\n\t\tc.Sort()\n\t}\n}\n\n// Spec returns this node specification.\nfunc (t *TreeNode) Spec() NodeSpec {\n\tvar gvrs client.GVRs\n\tvar paths, statuses []string\n\tfor parent := t; parent != nil; parent = parent.Parent {\n\t\tgvrs = append(gvrs, parent.GVR)\n\t\tpaths = append(paths, parent.ID)\n\t\tstatuses = append(statuses, parent.Extras[StatusKey])\n\t}\n\n\treturn NodeSpec{\n\t\tGVRs:     gvrs,\n\t\tPaths:    paths,\n\t\tStatuses: statuses,\n\t}\n}\n\n// Flatten returns a collection of node specs.\nfunc (t *TreeNode) Flatten() []NodeSpec {\n\trefs := make([]NodeSpec, 0, len(t.Children))\n\tfor _, c := range t.Children {\n\t\tif c.IsLeaf() {\n\t\t\trefs = append(refs, c.Spec())\n\t\t\tcontinue\n\t\t}\n\t\trefs = append(refs, c.Flatten()...)\n\t}\n\treturn refs\n}\n\n// Blank returns true if this node is unset.\nfunc (t *TreeNode) Blank() bool {\n\treturn t.GVR == client.NoGVR && t.ID == \"\"\n}\n\n// Hydrate hydrates a full tree bases on a collection of specifications.\nfunc Hydrate(specs []NodeSpec) *TreeNode {\n\troot := NewTreeNode(client.NoGVR, \"\")\n\tnav := root\n\tfor _, spec := range specs {\n\t\tfor i := len(spec.Paths) - 1; i >= 0; i-- {\n\t\t\tif nav.Blank() {\n\t\t\t\tnav.GVR, nav.ID, nav.Extras[StatusKey] = spec.GVRs[i], spec.Paths[i], spec.Statuses[i]\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tc := NewTreeNode(spec.GVRs[i], spec.Paths[i])\n\t\t\tc.Extras[StatusKey] = spec.Statuses[i]\n\t\t\tif n := nav.Find(spec.GVRs[i], spec.Paths[i]); n == nil {\n\t\t\t\tnav.Add(c)\n\t\t\t\tnav = c\n\t\t\t} else {\n\t\t\t\tnav = n\n\t\t\t}\n\t\t}\n\t\tnav = root\n\t}\n\n\treturn root\n}\n\n// Level computes the current node level.\nfunc (t *TreeNode) Level() int {\n\tvar level int\n\tp := t\n\tfor p != nil {\n\t\tp = p.Parent\n\t\tlevel++\n\t}\n\treturn level - 1\n}\n\n// MaxDepth computes the max tree depth.\nfunc (t *TreeNode) MaxDepth(depth int) int {\n\tmax := depth\n\tfor _, c := range t.Children {\n\t\tm := c.MaxDepth(depth + 1)\n\t\tif m > max {\n\t\t\tmax = m\n\t\t}\n\t}\n\treturn max\n}\n\n// Root returns the current tree root node.\nfunc (t *TreeNode) Root() *TreeNode {\n\tfor p := t; p != nil; p = p.Parent {\n\t\tif p.Parent == nil {\n\t\t\treturn p\n\t\t}\n\t}\n\treturn nil\n}\n\n// IsLeaf returns true if node has no children.\nfunc (t *TreeNode) IsLeaf() bool {\n\treturn t.CountChildren() == 0\n}\n\n// IsRoot returns true if node is top node.\nfunc (t *TreeNode) IsRoot() bool {\n\treturn t.Parent == nil\n}\n\n// ShallowClone performs a shallow node clone.\nfunc (t *TreeNode) ShallowClone() *TreeNode {\n\treturn &TreeNode{GVR: t.GVR, ID: t.ID, Extras: t.Extras}\n}\n\n// Filter filters the node based on query.\nfunc (t *TreeNode) Filter(q string, filter func(q, path string) bool) *TreeNode {\n\tspecs := t.Flatten()\n\tmatches := make([]NodeSpec, 0, len(specs))\n\tfor _, s := range specs {\n\t\tif filter(q, s.AsPath()+s.AsStatus()) {\n\t\t\tmatches = append(matches, s)\n\t\t}\n\t}\n\n\tif len(matches) == 0 {\n\t\treturn nil\n\t}\n\treturn Hydrate(matches)\n}\n\n// Add adds a new child node.\nfunc (t *TreeNode) Add(c *TreeNode) {\n\tc.Parent = t\n\tt.Children = append(t.Children, c)\n}\n\n// Clear delete all descendant nodes.\nfunc (t *TreeNode) Clear() {\n\tt.Children = []*TreeNode{}\n}\n\n// Find locates a node given a gvr/id spec.\nfunc (t *TreeNode) Find(gvr *client.GVR, id string) *TreeNode {\n\tif t.GVR == gvr && t.ID == id {\n\t\treturn t\n\t}\n\tfor _, c := range t.Children {\n\t\tif v := c.Find(gvr, id); v != nil {\n\t\t\treturn v\n\t\t}\n\t}\n\treturn nil\n}\n\n// Title computes the node title.\nfunc (t *TreeNode) Title(noIcons bool) string {\n\treturn t.computeTitle(noIcons)\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\n// Dump for debug...\nfunc (t *TreeNode) Dump() {\n\tdump(t, 0)\n}\n\nfunc dump(n *TreeNode, level int) {\n\tif n == nil {\n\t\tslog.Debug(\"NO DATA!!\")\n\t\treturn\n\t}\n\tslog.Debug(fmt.Sprintf(\"%s%s::%s\\n\", strings.Repeat(\"  \", level), n.GVR, n.ID))\n\tfor _, c := range n.Children {\n\t\tdump(c, level+1)\n\t}\n}\n\n// DumpStdOut to stdout for debug.\nfunc (t *TreeNode) DumpStdOut() {\n\tdumpStdOut(t, 0)\n}\n\nfunc dumpStdOut(n *TreeNode, level int) {\n\tif n == nil {\n\t\tfmt.Println(\"NO DATA!!\")\n\t\treturn\n\t}\n\tfmt.Printf(\"%s%s::%s\\n\", strings.Repeat(\"  \", level), n.GVR, n.ID)\n\tfor _, c := range n.Children {\n\t\tdumpStdOut(c, level+1)\n\t}\n}\n\nfunc category(gvr *client.GVR) string {\n\tmeta, err := dao.MetaAccess.MetaFor(gvr)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\treturn meta.SingularName\n}\n\nfunc (t TreeNode) computeTitle(noIcons bool) string {\n\tif !noIcons {\n\t\treturn t.toEmojiTitle()\n\t}\n\n\treturn t.toTitle()\n}\n\nconst (\n\ttitleFmt    = \" [gray::-]%s/[white::b][%s::b]%s[::]\"\n\ttopTitleFmt = \" [white::b][%s::b]%s[::]\"\n\ttoast       = \"TOAST\"\n)\n\nfunc (t TreeNode) toTitle() (title string) {\n\t_, n := client.Namespaced(t.ID)\n\tcolor, status := \"white\", \"OK\"\n\tif v, ok := t.Extras[StatusKey]; ok {\n\t\tswitch v {\n\t\tcase ToastStatus:\n\t\t\tcolor, status = \"orangered\", toast\n\t\tcase MissingRefStatus:\n\t\t\tcolor, status = \"orange\", toast+\"_REF\"\n\t\t}\n\t}\n\tdefer func() {\n\t\tif status != \"OK\" {\n\t\t\ttitle += fmt.Sprintf(\"  [gray::-][yellow:%s:b]%s[gray::-]\", color, status)\n\t\t}\n\t}()\n\n\tcateg := category(t.GVR)\n\tif categ == \"\" {\n\t\ttitle = fmt.Sprintf(topTitleFmt, color, n)\n\t} else {\n\t\ttitle = fmt.Sprintf(titleFmt, categ, color, n)\n\t}\n\n\tif !t.IsLeaf() {\n\t\ttitle += fmt.Sprintf(\"[white::d](%d[-::d])[-::-]\", t.CountChildren())\n\t}\n\n\tinfo, ok := t.Extras[InfoKey]\n\tif !ok {\n\t\treturn\n\t}\n\ttitle += fmt.Sprintf(\" [antiquewhite::][%s][::]\", info)\n\n\treturn\n}\n\nconst colorFmt = \"%s [%s::b]%s[::]\"\n\nfunc (t TreeNode) toEmojiTitle() (title string) {\n\t_, n := client.Namespaced(t.ID)\n\tcolor, status := \"white\", \"OK\"\n\tif v, ok := t.Extras[StatusKey]; ok {\n\t\tswitch v {\n\t\tcase ToastStatus:\n\t\t\tcolor, status = \"orangered\", toast\n\t\tcase MissingRefStatus:\n\t\t\tcolor, status = \"orange\", toast+\"_REF\"\n\t\t}\n\t}\n\tdefer func() {\n\t\tif status != \"OK\" {\n\t\t\ttitle += fmt.Sprintf(\" [gray::-][yellow:%s:b]%s[gray::-]\", color, status)\n\t\t}\n\t}()\n\n\ttitle = fmt.Sprintf(colorFmt, toEmoji(t.GVR), color, n)\n\tif !t.IsLeaf() {\n\t\ttitle += fmt.Sprintf(\"[white::d](%d[-::d])[-::-]\", t.CountChildren())\n\t}\n\n\tinfo, ok := t.Extras[InfoKey]\n\tif !ok {\n\t\treturn\n\t}\n\ttitle += fmt.Sprintf(\" [antiquewhite::][%s][::]\", info)\n\n\treturn\n}\n\nfunc toEmoji(gvr *client.GVR) string {\n\tif e := v1Emoji(gvr); e != \"\" {\n\t\treturn e\n\t}\n\tif e := appsEmoji(gvr); e != \"\" {\n\t\treturn e\n\t}\n\tif e := issueEmoji(gvr.String()); e != \"\" {\n\t\treturn e\n\t}\n\tswitch gvr {\n\tcase client.HpaGVR:\n\t\treturn \"♎️\"\n\tcase client.CrGVR, client.CrbGVR:\n\t\treturn \"👩‍\"\n\tcase client.RoGVR, client.RobGVR:\n\t\treturn \"👨🏻‍\"\n\tcase client.NpGVR:\n\t\treturn \"📕\"\n\tcase client.PdbGVR:\n\t\treturn \"🏷 \"\n\tcase client.PspGVR:\n\t\treturn \"👮‍♂️\"\n\tcase client.CoGVR:\n\t\treturn \"🐳\"\n\tcase client.NewGVR(\"report\"):\n\t\treturn \"🧼\"\n\tdefault:\n\t\treturn \"📎\"\n\t}\n}\n\nfunc issueEmoji(gvr string) string {\n\tswitch gvr {\n\tcase \"issue_0\":\n\t\treturn \"👍\"\n\tcase \"issue_1\":\n\t\treturn \"🔊\"\n\tcase \"issue_2\":\n\t\treturn \"☣️ \"\n\tcase \"issue_3\":\n\t\treturn \"🧨\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc v1Emoji(gvr *client.GVR) string {\n\tswitch gvr {\n\tcase client.NsGVR:\n\t\treturn \"🗂 \"\n\tcase client.NodeGVR:\n\t\treturn \"🖥 \"\n\tcase client.PodGVR:\n\t\treturn \"🚛\"\n\tcase client.SvcGVR:\n\t\treturn \"💁‍♀️\"\n\tcase client.SaGVR:\n\t\treturn \"💳\"\n\tcase client.PvGVR:\n\t\treturn \"📚\"\n\tcase client.PvcGVR:\n\t\treturn \"🎟 \"\n\tcase client.SecGVR:\n\t\treturn \"🔒\"\n\tcase client.CmGVR:\n\t\treturn \"🗺 \"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\nfunc appsEmoji(gvr *client.GVR) string {\n\tswitch gvr {\n\tcase client.DpGVR:\n\t\treturn \"🪂\"\n\tcase client.StsGVR:\n\t\treturn \"🎎\"\n\tcase client.DsGVR:\n\t\treturn \"😈\"\n\tcase client.RsGVR:\n\t\treturn \"👯‍♂️\"\n\tdefault:\n\t\treturn \"\"\n\t}\n}\n\n// EmojiInfo returns emoji help.\nfunc EmojiInfo() map[string]string {\n\tgvrs := []*client.GVR{\n\t\tclient.CoGVR,\n\t\tclient.NsGVR,\n\t\tclient.PodGVR,\n\t\tclient.SvcGVR,\n\t\tclient.SaGVR,\n\t\tclient.PvGVR,\n\t\tclient.PvcGVR,\n\t\tclient.SecGVR,\n\t\tclient.CmGVR,\n\t\tclient.DpGVR,\n\t\tclient.StsGVR,\n\t\tclient.DsGVR,\n\t}\n\n\tm := make(map[string]string, len(gvrs))\n\tfor _, gvr := range gvrs {\n\t\tm[gvr.R()] = toEmoji(gvr)\n\t}\n\n\treturn m\n}\n"
  },
  {
    "path": "internal/xray/tree_node_test.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage xray_test\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/derailed/k9s/internal/client\"\n\t\"github.com/derailed/k9s/internal/xray\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestTreeNodeCount(t *testing.T) {\n\tuu := map[string]struct {\n\t\troot *xray.TreeNode\n\t\te    int\n\t}{\n\t\t\"simple\": {\n\t\t\troot: root1(),\n\t\t\te:    3,\n\t\t},\n\t\t\"complex\": {\n\t\t\troot: root3(),\n\t\t\te:    26,\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.root.Count(client.NoGVR))\n\t\t})\n\t}\n}\n\nfunc TestTreeNodeFilter(t *testing.T) {\n\tuu := map[string]struct {\n\t\tq       string\n\t\troot, e *xray.TreeNode\n\t}{\n\t\t\"filter_simple\": {\n\t\t\troot: root1(),\n\t\t\te:    diff1(),\n\t\t\tq:    \"c1\",\n\t\t},\n\t\t\"filter_complex\": {\n\t\t\troot: root2(),\n\t\t\te:    diff2(),\n\t\t\tq:    \"c2\",\n\t\t},\n\t\t\"filter_no_match\": {\n\t\t\troot: root2(),\n\t\t\te:    nil,\n\t\t\tq:    \"bozo\",\n\t\t},\n\t\t\"filter_all_match\": {\n\t\t\troot: root2(),\n\t\t\te:    root2(),\n\t\t\tq:    \"\",\n\t\t},\n\t\t\"filter_complex1\": {\n\t\t\troot: root3(),\n\t\t\te:    diff3(),\n\t\t\tq:    \"coredns\",\n\t\t},\n\t}\n\n\trx := func(q, path string) bool {\n\t\trx := regexp.MustCompile(`(?i)` + q)\n\n\t\ttokens := strings.Split(path, \"::\")\n\t\tfor _, t := range tokens {\n\t\t\tif rx.MatchString(t) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t\treturn false\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tfiltered := u.root.Filter(u.q, rx)\n\t\t\tassert.Equal(t, u.e, filtered)\n\t\t})\n\t}\n}\n\nfunc TestTreeNodeHydrate(t *testing.T) {\n\tthreeOK := []string{\"ok\", \"ok\", \"ok\"}\n\tfiveOK := append(threeOK, \"ok\", \"ok\")\n\n\tuu := map[string]struct {\n\t\tspec []xray.NodeSpec\n\t\te    *xray.TreeNode\n\t}{\n\t\t\"flat_simple\": {\n\t\t\tspec: []xray.NodeSpec{\n\t\t\t\t{\n\t\t\t\t\tGVRs:     []*client.GVR{client.CoGVR, client.PodGVR},\n\t\t\t\t\tPaths:    []string{\"c1\", \"default/p1\"},\n\t\t\t\t\tStatuses: threeOK,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tGVRs:     []*client.GVR{client.CoGVR, client.PodGVR},\n\t\t\t\t\tPaths:    []string{\"c2\", \"default/p1\"},\n\t\t\t\t\tStatuses: threeOK,\n\t\t\t\t},\n\t\t\t},\n\t\t\te: root1(),\n\t\t},\n\t\t\"flat_complex\": {\n\t\t\tspec: []xray.NodeSpec{\n\t\t\t\t{\n\t\t\t\t\tGVRs:     []*client.GVR{client.SecGVR, client.CoGVR, client.PodGVR},\n\t\t\t\t\tPaths:    []string{\"s1\", \"c1\", \"default/p1\"},\n\t\t\t\t\tStatuses: threeOK,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tGVRs:     []*client.GVR{client.SecGVR, client.CoGVR, client.PodGVR},\n\t\t\t\t\tPaths:    []string{\"s2\", \"c2\", \"default/p1\"},\n\t\t\t\t\tStatuses: threeOK,\n\t\t\t\t},\n\t\t\t},\n\t\t\te: root2(),\n\t\t},\n\t\t\"complex1\": {\n\t\t\tspec: []xray.NodeSpec{\n\t\t\t\t{\n\t\t\t\t\tGVRs:     []*client.GVR{client.SecGVR, client.PodGVR, client.DpGVR, client.NsGVR, client.DpGVR},\n\t\t\t\t\tPaths:    []string{\"default/default-token-rr22g\", \"default/nginx-6b866d578b-c6tcn\", \"default/nginx\", \"-/default\", \"deployments\"},\n\t\t\t\t\tStatuses: fiveOK,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tGVRs:     []*client.GVR{client.CmGVR, client.PodGVR, client.DpGVR, client.NsGVR, client.DpGVR},\n\t\t\t\t\tPaths:    []string{\"kube-system/coredns\", \"kube-system/coredns-6955765f44-89q2p\", \"kube-system/coredns\", \"-/kube-system\", \"deployments\"},\n\t\t\t\t\tStatuses: fiveOK,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tGVRs:     []*client.GVR{client.SecGVR, client.PodGVR, client.DpGVR, client.NsGVR, client.DpGVR},\n\t\t\t\t\tPaths:    []string{\"kube-system/coredns-token-5cq9j\", \"kube-system/coredns-6955765f44-89q2p\", \"kube-system/coredns\", \"-/kube-system\", \"deployments\"},\n\t\t\t\t\tStatuses: fiveOK,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tGVRs:     []*client.GVR{client.CmGVR, client.PodGVR, client.DpGVR, client.NsGVR, client.DpGVR},\n\t\t\t\t\tPaths:    []string{\"kube-system/coredns\", \"kube-system/coredns-6955765f44-r9j9t\", \"kube-system/coredns\", \"-/kube-system\", \"deployments\"},\n\t\t\t\t\tStatuses: fiveOK,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tGVRs:     []*client.GVR{client.SecGVR, client.PodGVR, client.DpGVR, client.NsGVR, client.DpGVR},\n\t\t\t\t\tPaths:    []string{\"kube-system/coredns-token-5cq9j\", \"kube-system/coredns-6955765f44-r9j9t\", \"kube-system/coredns\", \"-/kube-system\", \"deployments\"},\n\t\t\t\t\tStatuses: fiveOK,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tGVRs:     []*client.GVR{client.SecGVR, client.PodGVR, client.DpGVR, client.NsGVR, client.DpGVR},\n\t\t\t\t\tPaths:    []string{\"kube-system/default-token-thzt8\", \"kube-system/metrics-server-6754dbc9df-88bk4\", \"kube-system/metrics-server\", \"-/kube-system\", \"deployments\"},\n\t\t\t\t\tStatuses: fiveOK,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tGVRs:     []*client.GVR{client.SecGVR, client.PodGVR, client.DpGVR, client.NsGVR, client.DpGVR},\n\t\t\t\t\tPaths:    []string{\"kube-system/nginx-ingress-token-kff5q\", \"kube-system/nginx-ingress-controller-6fc5bcc8c9-cwp55\", \"kube-system/nginx-ingress-controller\", \"-/kube-system\", \"deployments\"},\n\t\t\t\t\tStatuses: fiveOK,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tGVRs:     []*client.GVR{client.SecGVR, client.PodGVR, client.DpGVR, client.NsGVR, client.DpGVR},\n\t\t\t\t\tPaths:    []string{\"kubernetes-dashboard/kubernetes-dashboard-token-d6rt4\", \"kubernetes-dashboard/dashboard-metrics-scraper-7b64584c5c-c7b56\", \"kubernetes-dashboard/dashboard-metrics-scraper\", \"-/kubernetes-dashboard\", \"deployments\"},\n\t\t\t\t\tStatuses: fiveOK,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tGVRs:     []*client.GVR{client.SecGVR, client.PodGVR, client.DpGVR, client.NsGVR, client.DpGVR},\n\t\t\t\t\tPaths:    []string{\"kubernetes-dashboard/kubernetes-dashboard-token-d6rt4\", \"kubernetes-dashboard/kubernetes-dashboard-79d9cd965-b4c7d\", \"kubernetes-dashboard/kubernetes-dashboard\", \"-/kubernetes-dashboard\", \"deployments\"},\n\t\t\t\t\tStatuses: fiveOK,\n\t\t\t\t},\n\t\t\t},\n\t\t\te: root3(),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\troot := xray.Hydrate(u.spec)\n\t\t\tassert.Equal(t, u.e.Flatten(), root.Flatten())\n\t\t})\n\t}\n}\n\nfunc TestTreeNodeFlatten(t *testing.T) {\n\tuu := map[string]struct {\n\t\troot *xray.TreeNode\n\t\te    []xray.NodeSpec\n\t}{\n\t\t\"flat_simple\": {\n\t\t\troot: root1(),\n\t\t\te: []xray.NodeSpec{\n\t\t\t\t{\n\t\t\t\t\tGVRs:     []*client.GVR{client.CoGVR, client.PodGVR},\n\t\t\t\t\tPaths:    []string{\"c1\", \"default/p1\"},\n\t\t\t\t\tStatuses: []string{\"ok\", \"ok\"},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tGVRs:     []*client.GVR{client.CoGVR, client.PodGVR},\n\t\t\t\t\tPaths:    []string{\"c2\", \"default/p1\"},\n\t\t\t\t\tStatuses: []string{\"ok\", \"ok\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t\"flat_complex\": {\n\t\t\troot: root2(),\n\t\t\te: []xray.NodeSpec{\n\t\t\t\t{\n\t\t\t\t\tGVRs:     []*client.GVR{client.SecGVR, client.CoGVR, client.PodGVR},\n\t\t\t\t\tPaths:    []string{\"s1\", \"c1\", \"default/p1\"},\n\t\t\t\t\tStatuses: []string{\"ok\", \"ok\", \"ok\"},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tGVRs:     []*client.GVR{client.SecGVR, client.CoGVR, client.PodGVR},\n\t\t\t\t\tPaths:    []string{\"s2\", \"c2\", \"default/p1\"},\n\t\t\t\t\tStatuses: []string{\"ok\", \"ok\", \"ok\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tflat := u.root.Flatten()\n\t\t\tassert.Equal(t, u.e, flat)\n\t\t})\n\t}\n}\n\nfunc TestTreeNodeDiff(t *testing.T) {\n\tuu := map[string]struct {\n\t\tn1, n2 *xray.TreeNode\n\t\te      bool\n\t}{\n\t\t\"blank\": {\n\t\t\tn1: &xray.TreeNode{},\n\t\t\tn2: &xray.TreeNode{},\n\t\t},\n\t\t\"same\": {\n\t\t\tn1: xray.NewTreeNode(client.PodGVR, \"default/p1\"),\n\t\t\tn2: xray.NewTreeNode(client.PodGVR, \"default/p1\"),\n\t\t},\n\t}\n\n\tfor k := range uu {\n\t\tu := uu[k]\n\t\tt.Run(k, func(t *testing.T) {\n\t\t\tassert.Equal(t, u.e, u.n1.Diff(u.n2))\n\t\t})\n\t}\n}\n\nfunc TestTreeNodeClone(t *testing.T) {\n\tn := xray.NewTreeNode(client.PodGVR, \"default/p1\")\n\tc1 := xray.NewTreeNode(client.CoGVR, \"c1\")\n\tn.Add(c1)\n\n\tc := n.ShallowClone()\n\tassert.Equal(t, n.GVR, c.GVR)\n}\n\nfunc TestTreeNodeRoot(t *testing.T) {\n\tn := xray.NewTreeNode(client.PodGVR, \"default/p1\")\n\tc1 := xray.NewTreeNode(client.CoGVR, \"c1\")\n\tc2 := xray.NewTreeNode(client.CoGVR, \"c2\")\n\tn.Add(c1)\n\tn.Add(c2)\n\n\tassert.Equal(t, 2, n.CountChildren())\n\tassert.Equal(t, n, n.Root())\n\tassert.True(t, n.IsRoot())\n\tassert.False(t, n.IsLeaf())\n\tassert.Equal(t, n, c1.Root())\n\tassert.False(t, c1.IsRoot())\n\tassert.Equal(t, n, c2.Root())\n\tassert.True(t, c1.IsLeaf())\n}\n\nfunc TestTreeNodeLevel(t *testing.T) {\n\tn := xray.NewTreeNode(client.PodGVR, \"default/p1\")\n\tc1 := xray.NewTreeNode(client.CoGVR, \"c1\")\n\tc2 := xray.NewTreeNode(client.CoGVR, \"c2\")\n\tn.Add(c1)\n\tn.Add(c2)\n\n\tassert.Equal(t, 0, n.Level())\n\tassert.Equal(t, 1, c1.Level())\n\tassert.Equal(t, 1, c2.Level())\n}\n\nfunc TestTreeNodeMaxDepth(t *testing.T) {\n\tn := xray.NewTreeNode(client.PodGVR, \"default/p1\")\n\tc1 := xray.NewTreeNode(client.CoGVR, \"c1\")\n\tc2 := xray.NewTreeNode(client.CoGVR, \"c2\")\n\tn.Add(c1)\n\tn.Add(c2)\n\n\tassert.Equal(t, 1, n.MaxDepth(0))\n}\n\n// ----------------------------------------------------------------------------\n// Helpers...\n\nfunc root1() *xray.TreeNode {\n\tn := xray.NewTreeNode(client.PodGVR, \"default/p1\")\n\tc1 := xray.NewTreeNode(client.CoGVR, \"c1\")\n\tc2 := xray.NewTreeNode(client.CoGVR, \"c2\")\n\tn.Add(c1)\n\tn.Add(c2)\n\n\treturn n\n}\n\nfunc diff1() *xray.TreeNode {\n\tn := xray.NewTreeNode(client.PodGVR, \"default/p1\")\n\tc1 := xray.NewTreeNode(client.CoGVR, \"c1\")\n\tn.Add(c1)\n\n\treturn n\n}\n\nfunc root2() *xray.TreeNode {\n\tc1 := xray.NewTreeNode(client.CoGVR, \"c1\")\n\ts1 := xray.NewTreeNode(client.SecGVR, \"s1\")\n\tc1.Add(s1)\n\n\tc2 := xray.NewTreeNode(client.CoGVR, \"c2\")\n\ts2 := xray.NewTreeNode(client.SecGVR, \"s2\")\n\tc2.Add(s2)\n\n\tn := xray.NewTreeNode(client.PodGVR, \"default/p1\")\n\tn.Add(c1)\n\tn.Add(c2)\n\n\treturn n\n}\n\nfunc diff2() *xray.TreeNode {\n\tn := xray.NewTreeNode(client.PodGVR, \"default/p1\")\n\tc1 := xray.NewTreeNode(client.CoGVR, \"c2\")\n\tn.Add(c1)\n\n\ts1 := xray.NewTreeNode(client.SecGVR, \"s2\")\n\tc1.Add(s1)\n\n\treturn n\n}\n\nfunc root3() *xray.TreeNode {\n\tn := xray.NewTreeNode(client.DpGVR, \"deployments\")\n\n\tns1 := xray.NewTreeNode(client.NsGVR, \"-/default\")\n\tn.Add(ns1)\n\t{\n\t\td1 := xray.NewTreeNode(client.DpGVR, \"default/nginx\")\n\t\tns1.Add(d1)\n\t\t{\n\t\t\tp1 := xray.NewTreeNode(client.PodGVR, \"default/nginx-6b866d578b-c6tcn\")\n\t\t\td1.Add(p1)\n\t\t\t{\n\t\t\t\ts1 := xray.NewTreeNode(client.SecGVR, \"default/default-token-rr22g\")\n\t\t\t\tp1.Add(s1)\n\t\t\t}\n\t\t}\n\t}\n\n\tns2 := xray.NewTreeNode(client.NsGVR, \"-/kube-system\")\n\tn.Add(ns2)\n\t{\n\t\td2 := xray.NewTreeNode(client.DpGVR, \"kube-system/coredns\")\n\t\tns2.Add(d2)\n\t\t{\n\t\t\tp2 := xray.NewTreeNode(client.PodGVR, \"kube-system/coredns-6955765f44-89q2p\")\n\t\t\td2.Add(p2)\n\t\t\t{\n\t\t\t\tc1 := xray.NewTreeNode(client.CmGVR, \"kube-system/coredns\")\n\t\t\t\tp2.Add(c1)\n\t\t\t\ts2 := xray.NewTreeNode(client.SecGVR, \"kube-system/coredns-token-5cq9j\")\n\t\t\t\tp2.Add(s2)\n\t\t\t}\n\t\t\tp3 := xray.NewTreeNode(client.PodGVR, \"kube-system/coredns-6955765f44-r9j9t\")\n\t\t\td2.Add(p3)\n\t\t\t{\n\t\t\t\tc2 := xray.NewTreeNode(client.CmGVR, \"kube-system/coredns\")\n\t\t\t\tp3.Add(c2)\n\t\t\t\ts3 := xray.NewTreeNode(client.SecGVR, \"kube-system/coredns-token-5cq9j\")\n\t\t\t\tp3.Add(s3)\n\t\t\t}\n\t\t}\n\t\td3 := xray.NewTreeNode(client.DpGVR, \"kube-system/metrics-server\")\n\t\tns2.Add(d3)\n\t\t{\n\t\t\tp3 := xray.NewTreeNode(client.PodGVR, \"kube-system/metrics-server-6754dbc9df-88bk4\")\n\t\t\td3.Add(p3)\n\t\t\t{\n\t\t\t\ts4 := xray.NewTreeNode(client.SecGVR, \"kube-system/default-token-thzt8\")\n\t\t\t\tp3.Add(s4)\n\t\t\t}\n\t\t}\n\t\td4 := xray.NewTreeNode(client.DpGVR, \"kube-system/nginx-ingress-controller\")\n\t\tns2.Add(d4)\n\t\t{\n\t\t\tp4 := xray.NewTreeNode(client.PodGVR, \"kube-system/nginx-ingress-controller-6fc5bcc8c9-cwp55\")\n\t\t\td4.Add(p4)\n\t\t\t{\n\t\t\t\ts5 := xray.NewTreeNode(client.SecGVR, \"kube-system/nginx-ingress-token-kff5q\")\n\t\t\t\tp4.Add(s5)\n\t\t\t}\n\t\t}\n\t}\n\n\tns3 := xray.NewTreeNode(client.NsGVR, \"-/kubernetes-dashboard\")\n\tn.Add(ns3)\n\t{\n\t\td5 := xray.NewTreeNode(client.DpGVR, \"kubernetes-dashboard/dashboard-metrics-scraper\")\n\t\tns3.Add(d5)\n\t\t{\n\t\t\tp5 := xray.NewTreeNode(client.PodGVR, \"kubernetes-dashboard/dashboard-metrics-scraper-7b64584c5c-c7b56\")\n\t\t\td5.Add(p5)\n\t\t\t{\n\t\t\t\ts6 := xray.NewTreeNode(client.SecGVR, \"kubernetes-dashboard/kubernetes-dashboard-token-d6rt4\")\n\t\t\t\tp5.Add(s6)\n\t\t\t}\n\t\t}\n\t\td6 := xray.NewTreeNode(client.DpGVR, \"kubernetes-dashboard/kubernetes-dashboard\")\n\t\tns3.Add(d6)\n\t\t{\n\t\t\tp6 := xray.NewTreeNode(client.PodGVR, \"kubernetes-dashboard/kubernetes-dashboard-79d9cd965-b4c7d\")\n\t\t\td6.Add(p6)\n\t\t\t{\n\t\t\t\ts6 := xray.NewTreeNode(client.SecGVR, \"kubernetes-dashboard/kubernetes-dashboard-token-d6rt4\")\n\t\t\t\tp6.Add(s6)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn n\n}\n\nfunc diff3() *xray.TreeNode {\n\tn := xray.NewTreeNode(client.DpGVR, \"deployments\")\n\tns2 := xray.NewTreeNode(client.NsGVR, \"-/kube-system\")\n\tn.Add(ns2)\n\t{\n\t\td2 := xray.NewTreeNode(client.DpGVR, \"kube-system/coredns\")\n\t\tns2.Add(d2)\n\t\t{\n\t\t\tp2 := xray.NewTreeNode(client.PodGVR, \"kube-system/coredns-6955765f44-89q2p\")\n\t\t\td2.Add(p2)\n\t\t\t{\n\t\t\t\tc1 := xray.NewTreeNode(client.CmGVR, \"kube-system/coredns\")\n\t\t\t\tp2.Add(c1)\n\t\t\t\ts2 := xray.NewTreeNode(client.SecGVR, \"kube-system/coredns-token-5cq9j\")\n\t\t\t\tp2.Add(s2)\n\t\t\t}\n\t\t\tp3 := xray.NewTreeNode(client.PodGVR, \"kube-system/coredns-6955765f44-r9j9t\")\n\t\t\td2.Add(p3)\n\t\t\t{\n\t\t\t\tc2 := xray.NewTreeNode(client.CmGVR, \"kube-system/coredns\")\n\t\t\t\tp3.Add(c2)\n\t\t\t\ts3 := xray.NewTreeNode(client.SecGVR, \"kube-system/coredns-token-5cq9j\")\n\t\t\t\tp3.Add(s3)\n\t\t\t}\n\t\t}\n\t}\n\treturn n\n}\n"
  },
  {
    "path": "main.go",
    "content": "// SPDX-License-Identifier: Apache-2.0\n// Copyright Authors of K9s\n\npackage main\n\nimport (\n\t\"flag\"\n\t\"os\"\n\n\t\"github.com/derailed/k9s/cmd\"\n\t_ \"k8s.io/client-go/plugin/pkg/client/auth\"\n\t\"k8s.io/klog/v2\"\n)\n\nfunc init() {\n\tklog.InitFlags(nil)\n\n\tvar logFile string\n\tfor i, a := range os.Args {\n\t\tif a == \"--logFile\" && i+1 < len(os.Args) {\n\t\t\tlogFile = os.Args[i+1]\n\t\t\tbreak\n\t\t}\n\t}\n\tif logFile != \"\" {\n\t\tif err := flag.Set(\"log_file\", logFile); err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\tif err := flag.Set(\"logtostderr\", \"false\"); err != nil {\n\t\tpanic(err)\n\t}\n\tif err := flag.Set(\"alsologtostderr\", \"false\"); err != nil {\n\t\tpanic(err)\n\t}\n\tif err := flag.Set(\"stderrthreshold\", \"fatal\"); err != nil {\n\t\tpanic(err)\n\t}\n\tif err := flag.Set(\"v\", \"-10\"); err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc main() {\n\tcmd.Execute()\n}\n"
  },
  {
    "path": "plugins/README.md",
    "content": "# K9s community plugins\n\nK9s plugins extend the tool to provide additional functionality via actions to further help you observe or administer\nyour Kubernetes clusters.\n\nFollowing is an example of some plugin files in this directory. Other files are not listed in this table.\n\n| Plugin-Name                    | Description                                                                               | Available on Views                  | Shortcut    | Kubectl plugin, external dependencies                                                 |\n|--------------------------------|-------------------------------------------------------------------------------------------|-------------------------------------|-------------|---------------------------------------------------------------------------------------|\n| ai-incident-investigation.yaml | Run AI investigation on application issues to find the root cause in seconds              | all                                 | Shift-h/o   | [HolmesGPT](https://github.com/robusta-dev/holmesgpt)                                 |\n| argocd.yaml                    | Perform argocd operation quickly                                                          | applications                        | Shift-r     | [ArgoCD](https://argo-cd.readthedocs.io/en/stable/getting_started/)                   |\n| argo-workflows.yaml            | View, watch, terminate and trigger argo workflows                                         | workflows/workflowtemplates/cronworkflows | v/Shift-w/t/s | [Argo Workflows](https://argo-workflows.readthedocs.io/en/latest/)            |\n| crd-wizard.yaml                | Clear and intuitive interface for visualizing and exploring CR(D)s                        | applications                        | Shift-w     | [crd-wizard](https://github.com/pehlicd/crd-wizard)                                   |\n| debug-container.yaml           | Add [ephemeral debug container](1)<br>([nicolaka/netshoot](2))                            | containers                          | Shift-d     |                                                                                       |\n| dive.yaml                      | Dive image layers                                                                         | containers                          | d           | [Dive](https://github.com/wagoodman/dive)                                             |\n| dup.yaml                       | Duplicate, edit and Debug resources                                                       | all                                 | Shift-d/e/v | [dup](https://github.com/vash/dup)                                                    |\n| external-secrets.yaml          | Refresh external/push-secrets                                                             | externalsecrets/pushsecrets         | Shift-R     | [External Secrets](https://external-secrets.io)                                       |\n| get-all-namespace-resources.yaml  | List all namespace resources (using standard kubectl)                                  | all                                 | m           | [kubectl](https://kubernetes.io/docs/tasks/tools/) |\n| get-all.yaml                   | get all resources in a namespace                                                          | all                                 | g           | [Krew](https://krew.sigs.k8s.io/), [ketall](https://github.com/corneliusweig/ketall/) |\n| helm-diff.yaml                 | Diff with previous revision / current revision                                            | helm/history                        | Shift-D/Q   | [helm-diff](https://github.com/databus23/helm-diff)                                   |\n| job-suspend.yaml               | Suspends a running cronjob                                                                | cronjobs                            | Ctrl-s      |                                                                                       |\n| k3d-root-shell.yaml            | Root shell to k3d container                                                               | containers                          | Shift-s     | [jq](https://stedolan.github.io/jq/)                                                  |\n| keda-toggle.yaml               | Enable/disable [keda](3) ScaledObject autoscaler                                          | scaledobjects                       | Ctrl-N      |                                                                                       |\n| kube-metrics.yaml              | Visualize live pod/node metric graphs (Memory/CPU)                                        | pods/nodes                          | m           | [kube-metics](https://github.com/bakito/kube-metrics)                                 |\n| log-stern.yaml                 | View resource logs using stern                                                            | pods                                | Ctrl-l      |                                                                                       |\n| log-jq.yaml                    | View resource logs using jq                                                               | pods                                | Ctrl-j      | kubectl-plugins/kubectl-jq                                                            |\n| log-bunyan.yaml                | View pods, service, deployment logs using bunyan                                          | pods, service, deployment           | Ctrl-l      | [Bunyan](https://www.npmjs.com/package/bunyan)                                        |\n| log-full.yaml                  | get full logs from pod/container                                                          | pods/containers                     | Ctrl-l      |                                                                                       |\n| pvc-debug-container.yaml       | Add ephemeral debug container with pvc mounted                                            | pods                                | s           | kubectl                                                                               |\n| resource-recommendations.yaml  | View recommendations for CPU/Memory requests based on historical data                     | deployments/daemonsets/statefulsets | Shift-k     | [Robusta KRR](https://github.com/robusta-dev/krr)                                     |\n| szero.yaml                     | Temporarily scale down/up all deployments, statefulsets, and daemonsets                   | namespaces                          | Shift-d/u   | [szero](https://github.com/jadolg/szero)                                              |\n| trace-dns.yaml                 | Trace DNS resolution using Inspektor Gadget (4)                                           | containers/pods/nodes               | Shift-d     |                                                                                       |\n| vector-dev-top.yaml            | Run `vector top` in vector.dev container                                                  | pods/container                      | h           | [vector top](https://vector.dev/highlights/2020-12-23-vector-top/)                    |\n| start-alpine.yaml              | Starts a deployment for the `alpine:latest` docker image in the current namespace/context | deployments/pods                    | Ctrl-T      |                                                                                       |\n\n[1]: https://kubernetes.io/docs/tasks/debug/debug-application/debug-running-pod/#ephemeral-container\n\n[2]: https://github.com/nicolaka/netshoot\n\n[3]: https://keda.sh/\n\n[4]: https://inspektor-gadget.io/\n"
  },
  {
    "path": "plugins/ai-incident-investigation.yaml",
    "content": "plugins:\n# Author: Pavan Gudiwada\n# Investigate incidents in your cluster to quickly find the root cause using HolmesGPT \n# Requires HolmesGPT to be installed and configured (https://github.com/robusta-dev/holmesgpt) on your system\n# Open any K9s view, then:\n# Shift+H to run an investigation with default ask command\n# Shift+O to customize the question before running an investigation.\n  holmesgpt:\n    shortCut: Shift-H \n    description: Ask HolmesGPT \n    scopes:\n      - all \n    command: bash\n    background: false\n    confirm: false\n    args:\n      - -c\n      - |\n        holmes ask \"why is $NAME of $RESOURCE_NAME in -n $NAMESPACE not working as expected\"\n        \n        echo \"Press 'q' to exit\"\n        while : ; do\n        read -n 1 k <&1\n        if [[ $k = q ]] ; then\n        break\n        fi\n        done\n  custom-holmesgpt:\n    shortCut: Shift-Q\n    description: Custom HolmesGPT Ask\n    scopes:\n      - all \n    command: bash\n    background: false\n    confirm: false\n    args:\n      - -c\n      - |\n        INSTRUCTIONS=\"# Edit the line below. Lines starting with '#' will be ignored.\"\n        DEFAULT_ASK_COMMAND=\"why is $NAME of $RESOURCE_NAME in -n $NAMESPACE not working as expected\"\n\n        QUESTION_FILE=$(mktemp)\n\n        echo \"$INSTRUCTIONS\" > \"$QUESTION_FILE\"\n        echo \"$DEFAULT_ASK_COMMAND\" >> \"$QUESTION_FILE\"\n\n        # Open the line in the default text editor\n        ${EDITOR:-nano} \"$QUESTION_FILE\"\n\n        # Read the modified line, ignoring lines starting with '#'\n        user_input=$(grep -v '^#' \"$QUESTION_FILE\")\n\n        echo running: holmes ask \"\\\"$user_input\\\"\"\n        holmes ask \"$user_input\"\n        echo \"Press 'q' to exit\"\n        while : ; do\n        read -n 1 k <&1\n        if [[ $k = q ]] ; then\n        break\n        fi\n        done"
  },
  {
    "path": "plugins/argo-rollouts-powershell.yaml",
    "content": "# Manage argo-rollouts from PowerShell\r\n# See https://argoproj.github.io/argo-rollouts/\r\n# <g> Get rollout details\r\n# <w> Watch rollout progress\r\n# <p> (with confirmation) Promote rollout\r\n# <r> (with confirmation) Restart rollout\r\nplugins:\r\n  argo-rollouts-get:\r\n    shortCut: g\r\n    confirm: false\r\n    description: Get details\r\n    scopes:\r\n      - rollouts\r\n    command: powershell\r\n    background: false\r\n    args:\r\n      - kubectl\r\n      - argo \r\n      - rollouts \r\n      - get \r\n      - rollout \r\n      - $NAME \r\n      - --context \r\n      - $CONTEXT \r\n      - -n \r\n      - $NAMESPACE;\r\n      - $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')\r\n  argo-rollouts-watch:\r\n    shortCut: w\r\n    confirm: false\r\n    description: Watch progress\r\n    scopes:\r\n      - rollouts\r\n    command: powershell\r\n    background: false\r\n    args:\r\n      - kubectl \r\n      - argo \r\n      - rollouts \r\n      - get \r\n      - rollout \r\n      - $NAME \r\n      - --context \r\n      - $CONTEXT \r\n      - -n \r\n      - $NAMESPACE \r\n      - -w;\r\n      - $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')\r\n  argo-rollouts-promote:\r\n    shortCut: p\r\n    confirm: true\r\n    description: Promote\r\n    scopes:\r\n      - rollouts\r\n    command: powershell\r\n    background: false\r\n    args:\r\n      - kubectl \r\n      - argo \r\n      - rollouts \r\n      - promote \r\n      - $NAME \r\n      - --context \r\n      - $CONTEXT \r\n      - -n \r\n      - $NAMESPACE;\r\n      - $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')\r\n  argo-rollouts-restart:\r\n    shortCut: r\r\n    confirm: true\r\n    description: Restart\r\n    scopes:\r\n      - rollouts\r\n    command: powershell\r\n    background: false\r\n    args:\r\n      - kubectl \r\n      - argo \r\n      - rollouts \r\n      - restart \r\n      - $NAME \r\n      - --context \r\n      - $CONTEXT \r\n      - -n \r\n      - $NAMESPACE;\r\n      - $null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown')"
  },
  {
    "path": "plugins/argo-rollouts.yaml",
    "content": "# Manage argo-rollouts\n# See https://argoproj.github.io/argo-rollouts/\n# <g> Get rollout details\n# <w> Watch rollout progress\n# <p> (with confirmation) Promote rollout\n# <r> (with confirmation) Restart rollout\nplugins:\n  argo-rollouts-get:\n    shortCut: g\n    confirm: false\n    description: Get details\n    scopes:\n      - rollouts\n    command: bash\n    background: false\n    args:\n      - -c\n      - kubectl argo rollouts get rollout $NAME --context $CONTEXT -n $NAMESPACE |& less\n  argo-rollouts-watch:\n    shortCut: w\n    confirm: false\n    description: Watch progress\n    scopes:\n      - rollouts\n    command: bash\n    background: false\n    args:\n      - -c\n      - kubectl argo rollouts get rollout $NAME --context $CONTEXT -n $NAMESPACE -w\n  argo-rollouts-promote:\n    shortCut: p\n    confirm: true\n    description: Promote\n    scopes:\n      - rollouts\n    command: bash\n    background: false\n    args:\n      - -c\n      - kubectl argo rollouts promote $NAME --context $CONTEXT -n $NAMESPACE |& less\n  argo-rollouts-restart:\n    shortCut: r\n    confirm: true\n    description: Restart\n    scopes:\n      - rollouts\n    command: bash\n    background: false\n    args:\n      - -c\n      - kubectl argo rollouts restart $NAME --context $CONTEXT -n $NAMESPACE |& less\n"
  },
  {
    "path": "plugins/argo-workflows.yaml",
    "content": "# Plugin to interact with argo workflows directly from k9s.\n# As the plugin acts as a wrapper around the argo (workflows) CLI, it must be installed for\n# the plugin to work. To install it, see https://github.com/argoproj/argo-workflows/releases/\nplugins:\n  view-workflow:\n    shortCut: v\n    confirm: false\n    description: view\n    scopes:\n      - workflows\n    command: sh\n    background: false\n    args:\n      - -c\n      - |\n        argo -n $NAMESPACE get $NAME\n        echo -n \"\\nPress enter to return to k9s...\" && read _\n  watch-workflow:\n    shortCut: Shift-W\n    confirm: false\n    description: watch\n    scopes:\n      - workflows\n    command: sh\n    background: false\n    args:\n      - -c\n      - \"watch -n 3 -q 5 argo -n $NAMESPACE get $NAME\"\n  terminate-workflow:\n    shortCut: t\n    confirm: true\n    description: terminate\n    scopes:\n      - workflows\n    command: argo\n    background: false\n    args:\n      - -n \n      - $NAMESPACE \n      - terminate \n      - $NAME\n  submit-from:\n    shortCut: s\n    confirm: true\n    description: submit-as-workflow\n    scopes:\n      - workflowtemplates\n      - cronworkflows\n    command: argo\n    background: false\n    args:\n      - -n \n      - $NAMESPACE \n      - submit \n      - --from\n      - $RESOURCE_NAME/$NAME\n"
  },
  {
    "path": "plugins/argocd.yaml",
    "content": "plugins:\n  argocd:\n    shortCut: \"s\"\n    description: Sync ArgoCD Application\n    scopes:\n      - application\n    command: argocd\n    args: \n    - app\n    - sync\n    - $NAME\n    - --app-namespace\n    - $NAMESPACE\n    background: true\n    confirm: true\n\n  refresh-apps:\n    shortCut: Shift-R\n    confirm: false\n    scopes:\n      - apps\n    description: Refresh a argocd app hard\n    command: bash\n    background: false\n    args:\n      - -c\n      - \"kubectl annotate applications -n argocd $NAME argocd.argoproj.io/refresh=hard\"\n\n  disable-auto-sync:\n    shortCut: Shift-J\n    confirm: false\n    scopes:\n      - apps\n    description: Disable argocd sync\n    command: kubectl\n    background: false\n    args:\n      - patch\n      - applications\n      - -n\n      - argocd\n      - $NAME\n      - \"--type=json\"\n      - '-p=[{\"op\":\"replace\", \"path\": \"/spec/syncPolicy\", \"value\": {}}]'\n\n  enable-auto-sync:\n    shortCut: Shift-B\n    confirm: false\n    scopes:\n      - apps\n    description: Enable argocd sync\n    command: kubectl\n    background: false\n    args:\n      - patch\n      - applications\n      - -n\n      - argocd\n      - $NAME\n      - --type=merge\n      - '-p={\"spec\":{\"syncPolicy\":{\"automated\":{\"prune\":true,\"selfHeal\":true},\"syncOptions\":[\"ApplyOutOfSyncOnly=true\",\"CreateNamespace=true\",\"PruneLast=true\",\"PrunePropagationPolicy=foreground\"]}}}'\n"
  },
  {
    "path": "plugins/blame.yaml",
    "content": "plugins:\n  # kubectl-blame by knight42\n  # Annotate each line in the given resource's YAML with information from the managedFields to show who last modified the field.\n  # Source: https://github.com/knight42/kubectl-blame\n  # Install via:\n  #   krew: `kubectl krew install blame`\n  #   go: `go install github.com/knight42/kubectl-blame@latest`\n  blame:\n    shortCut: b\n    confirm: false\n    description: \"Blame\"\n    scopes:\n      - all\n    command: sh\n    background: false\n    args:\n      - -c\n      - \"kubectl-blame $RESOURCE_NAME $NAME -n $NAMESPACE --context $CONTEXT | less\"\n"
  },
  {
    "path": "plugins/carvel.yaml",
    "content": "# $HOME/.k9s/plugin.yml\nplugins:\n  kapp-inspect:\n    shortCut: Shift-Z\n    confirm: false\n    description: Kapp inspect\n    scopes:\n      - app\n    command: bash\n    background: false\n    args:\n      - -c\n      - \"export FORCE_COLOR=1;kapp inspect -a $NAME.app --namespace $NAMESPACE --kubeconfig-context $CONTEXT --color --tty | less -RK\"\n  kctrl-app-status:\n    shortCut: Shift-Q\n    confirm: false\n    description: kctrl app status\n    scopes:\n      - app\n    command: bash\n    background: false\n    args:\n      - -c\n      - \"export FORCE_COLOR=1;kctrl app status -a $NAME --namespace $NAMESPACE --kubeconfig-context $CONTEXT --color --tty | less -RK\"\n  kctrl-app-pause:\n    shortCut: Shift-T\n    confirm: false\n    description: kctrl app pause\n    scopes:\n      - app\n    command: bash\n    background: false\n    args:\n      - -c\n      - \"export FORCE_COLOR=1;kctrl app pause -a $NAME --namespace $NAMESPACE --kubeconfig-context $CONTEXT --yes --color --tty | less -RK\"\n  kctrl-app-kick:\n    shortCut: Shift-K\n    confirm: false\n    description: kctrl app kick\n    scopes:\n      - app\n    command: bash\n    background: false\n    args:\n      - -c\n      - \"export FORCE_COLOR=1;kctrl app kick -a $NAME --namespace $NAMESPACE --kubeconfig-context $CONTEXT --yes --color --tty | less -RK\"\n"
  },
  {
    "path": "plugins/cert-manager.yaml",
    "content": "# Manage cert-manager Certificate resources via cmctl.\n# See: https://github.com/cert-manager/cmctl\nplugins:\n  cert-status:\n    shortCut: Shift-S\n    confirm: false\n    description: Certificate status\n    scopes:\n      - certificates\n    command: bash\n    background: false\n    args:\n      - -c\n      - \"cmctl status certificate --context $CONTEXT -n $NAMESPACE $NAME |& less\"\n  cert-renew:\n    shortCut: Shift-R\n    confirm: false\n    description: Certificate renew\n    scopes:\n      - certificates\n    command: bash\n    background: false\n    args:\n      - -c\n      - \"cmctl renew --context $CONTEXT -n $NAMESPACE $NAME |& less\"\n  secret-inspect:\n    shortCut: Shift-I\n    confirm: false\n    description: Inspect secret\n    scopes:\n      - secrets\n    command: bash\n    background: false\n    args:\n      - -c\n      - \"cmctl inspect secret --context $CONTEXT -n $NAMESPACE $NAME |& less\""
  },
  {
    "path": "plugins/crd-wizard.yaml",
    "content": "# See: https://github.com/pehlicd/crd-wizard\nplugins:\n  crd-wizard:\n    shortCut: Shift-W\n    description: CRD Wizard\n    dangerous: false\n    scopes:\n      - crds\n    command: bash\n    background: false\n    confirm: false\n    args:\n      - -c\n      - \"crd-wizard tui --context $CONTEXT --kind $COL-KIND\""
  },
  {
    "path": "plugins/crossplane.yaml",
    "content": "plugins:\n  # crossplane-trace list all the relationships with a resource (Claim, Composite, or Managed Resource)\n  # Requires 'crossplane' cli binary installed\n  crossplane-trace:\n    shortCut: t\n    confirm: false\n    description: \"Crossplane Trace\"\n    scopes:\n      - all\n    command: sh\n    background: false\n    args:\n      - -c\n      - |\n        if [ -n \"$NAMESPACE\" ]; then\n          crossplane beta trace --context $CONTEXT -n $NAMESPACE $RESOURCE_NAME.$RESOURCE_GROUP $NAME -owide | less -K\n        else\n          crossplane beta trace --context $CONTEXT $RESOURCE_NAME.$RESOURCE_GROUP $NAME -owide | less -K\n        fi\n  # crossplane-watch requires 'crossplane' cli and 'viddy' binaries installed\n  # 'viddy' is a modern implementation of 'watch' command written in rust. Read more on https://github.com/sachaos/viddy.\n  crossplane-watch:\n    shortCut: w\n    confirm: false\n    description: \"Crossplane Watch\"\n    scopes:\n      - all\n    command: sh\n    background: false\n    args:\n      - -c\n      - |\n        if [ -n \"$NAMESPACE\" ]; then\n          viddy -pw 'crossplane beta trace --context $CONTEXT -n $NAMESPACE $RESOURCE_NAME.$RESOURCE_GROUP $NAME -owide'\n        else\n          viddy -pw 'crossplane beta trace --context $CONTEXT $RESOURCE_NAME.$RESOURCE_GROUP $NAME -owide'\n        fi\n"
  },
  {
    "path": "plugins/current-ctx-terminal.yaml",
    "content": "plugins:  \n  open-terminal:\n    shortCut: Ctrl-T\n    confirm: false\n    description: Open a terminal in the current context\n    scopes:\n      - all\n    command: /usr/bin/sh\n    background: false\n    args:\n      - -c\n      - bash -c \"kubectl config use-context $CONTEXT && echo -e \\\"\\e[1;42mk9s bash terminal.\\nCtrl + d or 'exit' to go back to k9s\\e[0m\\\" && bash\"\n      # New window for terminal can be opened with any emulator\n      #- x-terminal-emulator -e bash -c \"kubectl config use-context $CONTEXT && echo -e \\\"\\e[1;42mk9s bash terminal.\\nCtrl + d or 'exit' to go back to k9s\\e[0m\\\" && bash\"\n      # example with tilix:\n      #- tilix -e bash -c \"kubectl config use-context $CONTEXT && echo -e \\\"\\e[1;42mk9s bash terminal.\\nCtrl + d or 'exit' to go back to k9s\\e[0m\\\" && bash\""
  },
  {
    "path": "plugins/debug-container.yaml",
    "content": "plugins:\n  #--- Create debug container for selected pod in current namespace\n  # See https://kubernetes.io/docs/tasks/debug/debug-application/debug-running-pod/#ephemeral-container\n  debug:\n    shortCut: Shift-D\n    description: Add debug container\n    dangerous: true\n    scopes:\n      - containers\n    command: bash\n    background: false\n    confirm: true\n    inputs:\n      - name: image\n        label: Debug image\n        type: dropdown\n        required: true\n        default: nicolaka/netshoot:v0.15\n        options:\n          - nicolaka/netshoot:v0.15\n          - busybox:1.37\n          - alpine:3.23\n          - ubuntu:26.04\n      - name: profile\n        label: Debug profile\n        type: dropdown\n        required: true\n        default: sysadmin\n        options:\n          - general\n          - baseline\n          - restricted\n          - netadmin\n          - sysadmin\n          - legacy\n      - name: share_processes\n        label: Share processes\n        type: bool\n        required: true\n        default: true\n    args:\n      - -c\n      - >-\n        kubectl debug -it --context $CONTEXT -n=$NAMESPACE $POD\n        --target=$NAME\n        --image=$INPUT_IMAGE\n        --profile=$INPUT_PROFILE\n        $([ \"$INPUT_SHARE_PROCESSES\" = \"true\" ] && echo \"--share-processes\")\n        -- sh\n\n"
  },
  {
    "path": "plugins/dive.yaml",
    "content": "plugins:\n  dive:\n    shortCut: d\n    confirm: false\n    description: \"Dive image\"\n    scopes:\n      - containers\n    command: dive\n    background: false\n    args:\n      - $COL-IMAGE\n"
  },
  {
    "path": "plugins/dup.yaml",
    "content": "plugins:\n  dup:\n    shortCut: Shift-D\n    description: Duplicate\n    scopes:\n      - all\n    command: bash\n    background: false\n    confirm: true\n    args:\n      - -c\n      - \"kubectl dup -k $RESOURCE_NAME $NAME -n $NAMESPACE --context $CONTEXT\"\n  dup_edit:\n    # Prompted to edit a new duplicate resource\n    shortCut: Shift-E\n    description: Duplicate + Edit\n    scopes:\n      - all\n    command: bash\n    background: false\n    confirm: true\n    args:\n      - -c\n      - \"kubectl dup $RESOURCE_NAME $NAME -n $NAMESPACE --context $CONTEXT\"\n  dup_debug_pods:\n    # Spawn a resource with no readiness probes and infinite running command.\n    shortCut: Shift-V\n    description: Debug\n    scopes:\n      - pods\n    command: bash\n    background: true\n    confirm: true\n    args:\n      - -c\n      - \"kubectl dup -kdl $RESOURCE_NAME $NAME -n $NAMESPACE --context $CONTEXT\"\n  dup_debug_deploy:\n    # Spawn a resource with no readiness probes and infinite running command.\n    shortCut: Shift-V\n    description: Debug\n    scopes:\n      - deployments\n    command: bash\n    background: true\n    confirm: true\n    args:\n      - -c\n      - \"kubectl dup -kdl $RESOURCE_NAME $NAME -n $NAMESPACE --context $CONTEXT\"\n  dup_debug_sts:\n    # Spawn a resource with no readiness probes and infinite running command.\n    shortCut: Shift-V\n    description: Debug\n    scopes:\n      - statefulsets\n    command: bash\n    background: true\n    confirm: true\n    args:\n      - -c\n      - \"kubectl dup -kdl $RESOURCE_NAME $NAME -n $NAMESPACE --context $CONTEXT\"\n  dup_debug_cronjob:\n    # Spawn a resource with no readiness probes and infinite running command.\n    shortCut: Shift-V\n    description: Debug\n    scopes:\n      - cronjobs\n    command: bash\n    background: true\n    confirm: true\n    args:\n      - -c\n      - \"kubectl dup -kdl $RESOURCE_NAME $NAME -n $NAMESPACE --context $CONTEXT\"\n  dup_debug_jobs:\n    # Spawn a resource with no readiness probes and infinite running command.\n    shortCut: Shift-V\n    description: Debug\n    scopes:\n      - jobs\n    command: bash\n    background: true\n    confirm: true\n    args:\n      - -c\n      - \"kubectl dup -kdl $RESOURCE_NAME $NAME -n $NAMESPACE --context $CONTEXT\"\n"
  },
  {
    "path": "plugins/duplik8s.yaml",
    "content": "# Duplicate Pods, Deployments and StatefulSet for easy debugging\n# and troubleshooting.\n#\n# See https://github.com/Telemaco019/duplik8s\nplugins:\n  duplicate:\n    shortCut: Ctrl-B\n    description: Duplicate resource\n    scopes:\n      - po\n      - deploy\n      - statefulset\n    command: kubectl\n    background: true\n    args:\n      - duplicate\n      - $RESOURCE_NAME\n      - $NAME\n      - -n\n      - $NAMESPACE\n      - --context\n      - $CONTEXT"
  },
  {
    "path": "plugins/eks-node-viewer.yaml",
    "content": "# plugin to easily open eks-node-viewer on viewed context\n# requires eks-node-viewer installed on system\n# https://github.com/awslabs/eks-node-viewer/\nplugins:\n  eks-node-viewer:\n    shortCut: Shift-X\n    description: \"eks-node-viewer\"\n    scopes:\n      - node\n    background: false\n    command: bash\n    args:\n    - -c\n    - |\n      env $(kubectl config view --context $CONTEXT --minify -o json | jq -r \".users[0].user.exec.env[] | select(.name == \\\"AWS_PROFILE\\\") | \\\"AWS_PROFILE=\\\" + .value\" && kubectl config view --context $CONTEXT --minify -o json | jq -r \".users[0].user.exec.args | \\\"AWS_REGION=\\\" + .[1]\") eks-node-viewer --context $CONTEXT --resources cpu,memory --extra-labels karpenter.sh/nodepool,eks-node-viewer/node-age --node-sort=creation=dsc\n"
  },
  {
    "path": "plugins/external-secrets.yaml",
    "content": "plugins:\n  refresh-external-secrets:\n    shortCut: Shift-R\n    confirm: false\n    scopes:\n      - externalsecrets\n    description: Refresh the externalsecret\n    command: bash\n    background: true\n    args:\n      - -c\n      - \"kubectl annotate externalsecrets.external-secrets.io --context $CONTEXT -n $NAMESPACE $NAME force-sync=$(date +%s) --overwrite\"\n  refresh-push-secrets:\n    shortCut: Shift-R\n    confirm: false\n    scopes:\n      - pushsecrets\n    description: Refresh the pushsecret\n    command: bash\n    background: true\n    args:\n      - -c\n      - \"kubectl annotate pushsecrets.external-secrets.io --context $CONTEXT -n $NAMESPACE $NAME force-sync=$(date +%s) --overwrite\"\n"
  },
  {
    "path": "plugins/flux.yaml",
    "content": "# $HOME/.k9s/plugin.yml\n# move selected line to chosen resource in K9s, then:\n# Shift-T (with confirmation) to toggle helm releases or kustomizations suspend and resume\n# Shift-R (no confirmation) to reconcile a git source or a helm release or a kustomization\nplugins:\n  toggle-helmrelease:\n    shortCut: Shift-T\n    confirm: true\n    scopes:\n      - helmreleases\n    description: Toggle to suspend or resume a HelmRelease\n    command: bash\n    background: false\n    args:\n      - -c\n      - >-\n        suspended=$(kubectl --context $CONTEXT get helmreleases -n $NAMESPACE $NAME -o=custom-columns=TYPE:.spec.suspend | tail -1);\n        verb=$([ $suspended = \"true\" ] && echo \"resume\" || echo \"suspend\");\n        flux\n        $verb helmrelease\n        --context $CONTEXT\n        -n $NAMESPACE $NAME\n        | less -K\n  toggle-kustomization:\n    shortCut: Shift-T\n    confirm: true\n    scopes:\n      - kustomizations\n    description: Toggle to suspend or resume a Kustomization\n    command: bash\n    background: false\n    args:\n      - -c\n      - >-\n        suspended=$(kubectl --context $CONTEXT get kustomizations -n $NAMESPACE $NAME -o=custom-columns=TYPE:.spec.suspend | tail -1);\n        verb=$([ $suspended = \"true\" ] && echo \"resume\" || echo \"suspend\");\n        flux\n        $verb kustomization\n        --context $CONTEXT\n        -n $NAMESPACE $NAME\n        | less -K\n  reconcile-git:\n    shortCut: Shift-R\n    confirm: false\n    description: Flux reconcile\n    scopes:\n      - gitrepositories\n    command: bash\n    background: false\n    args:\n      - -c\n      - >-\n        flux\n        reconcile source git\n        --context $CONTEXT\n        -n $NAMESPACE $NAME\n        | less -K\n  reconcile-hr:\n    shortCut: Shift-R\n    confirm: false\n    description: Flux reconcile\n    scopes:\n      - helmreleases\n    command: bash\n    background: false\n    args:\n      - -c\n      - >-\n        flux\n        reconcile helmrelease\n        --context $CONTEXT\n        -n $NAMESPACE $NAME\n        | less -K\n  reconcile-helm-repo:\n    shortCut: Shift-Z\n    description: Flux reconcile\n    scopes:\n      - helmrepositories\n    command: bash\n    background: false\n    confirm: false\n    args:\n      - -c\n      - >-\n        flux\n        reconcile source helm\n        --context $CONTEXT\n        -n $NAMESPACE $NAME\n        | less -K\n  reconcile-oci-repo:\n    shortCut: Shift-Z\n    description: Flux reconcile\n    scopes:\n      - ocirepositories\n    command: bash\n    background: false\n    confirm: false\n    args:\n      - -c\n      - >-\n        flux\n        reconcile source oci\n        --context $CONTEXT\n        -n $NAMESPACE $NAME\n        | less -K\n  reconcile-ks:\n    shortCut: Shift-R\n    confirm: false\n    description: Flux reconcile\n    scopes:\n      - kustomizations\n    command: bash\n    background: false\n    args:\n      - -c\n      - >-\n        flux\n        reconcile kustomization\n        --context $CONTEXT\n        -n $NAMESPACE $NAME\n        | less -K\n  reconcile-ir:\n    shortCut: Shift-R\n    confirm: false\n    description: Flux reconcile\n    scopes:\n      - imagerepositories\n    command: sh\n    background: false\n    args:\n      - -c\n      - >-\n        flux\n        reconcile image repository\n        --context $CONTEXT\n        -n $NAMESPACE $NAME\n        | less -K\n  reconcile-iua:\n    shortCut: Shift-R\n    confirm: false\n    description: Flux reconcile\n    scopes:\n      - imageupdateautomations\n    command: sh\n    background: false\n    args:\n      - -c\n      - >-\n        flux\n        reconcile image update\n        --context $CONTEXT\n        -n $NAMESPACE $NAME\n        | less -K\n  toggle-rset:\n    shortCut: Shift-T\n    confirm: false\n    scopes:\n      - resourcesets\n    description: Toggle to suspend or resume a ResourceSet\n    command: bash\n    background: false\n    args:\n      - -c\n      - >-\n        reconcile=$(kubectl --context $CONTEXT get resourceset -n $NAMESPACE $NAME -o=custom-columns='TYPE:.metadata.annotations.fluxcd\\.controlplane\\.io/reconcile' | tail -1);\n        verb=$([ $reconcile = \"disabled\" ] && echo \"resume\" || echo \"suspend\");\n        flux-operator\n        $verb rset\n        --kube-context $CONTEXT\n        -n $NAMESPACE $NAME\n        | less -K\n  toggle-inputprovider:\n    shortCut: Shift-T\n    confirm: false\n    scopes:\n      - resourcesetinputprovider\n    description: Toggle to suspend or resume an InputProvider\n    command: bash\n    background: false\n    args:\n      - -c\n      - >-\n        reconcile=$(kubectl --context $CONTEXT get resourcesetinputprovider -n $NAMESPACE $NAME -o=custom-columns='TYPE:.metadata.annotations.fluxcd\\.controlplane\\.io/reconcile' | tail -1);\n        verb=$([ $reconcile = \"disabled\" ] && echo \"resume\" || echo \"suspend\");\n        flux-operator\n        $verb inputprovider\n        --kube-context $CONTEXT\n        -n $NAMESPACE $NAME\n        | less -K\n  reconcile-rset:\n    shortCut: Shift-R\n    confirm: false\n    description: Flux reconcile\n    scopes:\n      - resourcesets\n    command: bash\n    background: false\n    args:\n      - -c\n      - >-\n        flux-operator\n        reconcile rset\n        --kube-context $CONTEXT\n        -n $NAMESPACE $NAME\n        | less -K\n  reconcile-inputprovider:\n    shortCut: Shift-R\n    confirm: false\n    description: Flux reconcile\n    scopes:\n      - resources\n    command: bash\n    background: false\n    args:\n      - -c\n      - >-\n        flux-operator\n        reconcile inputprovider\n        --kube-context $CONTEXT\n        -n $NAMESPACE $NAME\n        | less -K\n  reconcile-fluxinstance:\n    shortCut: Shift-R\n    confirm: false\n    description: Flux reconcile\n    scopes:\n      - fluxinstances\n    command: bash\n    background: false\n    args:\n      - -c\n      - >-\n        flux-operator\n        reconcile instance\n        --kube-context $CONTEXT\n        -n $NAMESPACE $NAME\n        | less -K\n  trace:\n    shortCut: Shift-Q\n    confirm: false\n    description: Flux trace\n    scopes:\n      - all\n    command: bash\n    background: false\n    args:\n      - -c\n      - >-\n        if [ -n \"$RESOURCE_GROUP\" ]; then api_endpoint=\"/apis/$RESOURCE_GROUP/$RESOURCE_VERSION\"; else api_endpoint=\"/api/$RESOURCE_VERSION\"; fi;\n        api_resource=$(kubectl get --raw \"${api_endpoint}\" | jq -r \".resources[] | select(.name==\\\"$RESOURCE_NAME\\\")\");\n        kind=$(echo ${api_resource} | jq -r '.kind');\n        namespace_arg=$(echo ${api_resource} | jq -r \"if .namespaced == true then \\\"--namespace $NAMESPACE\\\" else \\\"\\\" end\");\n        [ -n \"$RESOURCE_GROUP\" ] && api_version=$RESOURCE_GROUP/;\n        api_version=${api_version}$RESOURCE_VERSION;\n        flux\n        trace\n        --context $CONTEXT\n        --kind ${kind}\n        --api-version ${api_version}\n        ${namespace_arg}\n        $NAME\n        |& less -K\n  # credits: https://github.com/fluxcd/flux2/discussions/2494\n  get-suspended-helmreleases:\n    shortCut: Shift-S\n    confirm: false\n    description: Suspended Helm Releases\n    scopes:\n      - helmrelease\n    command: sh\n    background: false\n    args:\n      - -c\n      - >-\n        kubectl get\n        --context $CONTEXT\n        --all-namespaces\n        helmreleases.helm.toolkit.fluxcd.io -o json\n        | jq -r '.items[] | select(.spec.suspend==true) | [.metadata.namespace,.metadata.name,.spec.suspend] | @tsv'\n        | less -K\n  get-suspended-kustomizations:\n    shortCut: Shift-S\n    confirm: false\n    description: Suspended Kustomizations\n    scopes:\n      - kustomizations\n    command: sh\n    background: false\n    args:\n      - -c\n      - >-\n        kubectl get\n        --context $CONTEXT\n        --all-namespaces\n        kustomizations.kustomize.toolkit.fluxcd.io -o json\n        | jq -r '.items[] | select(.spec.suspend==true) | [.metadata.name,.spec.suspend] | @tsv'\n        | less -K\n"
  },
  {
    "path": "plugins/get-all-namespace-resources.yaml",
    "content": "plugins:\n  get-all-resources-by-selected-namespace:\n    shortCut: m\n    confirm: false\n    description: List all resources of the selected namespace\n    scopes:\n      - namespaces\n    command: sh\n    background: false\n    args:\n      - -c\n      - 'for r in $(kubectl api-resources --verbs=list --namespaced -o name); do out=$(kubectl get --ignore-not-found --show-kind -n $NAME $r 2>/dev/null); if [ -n \"$out\" ]; then echo \"$out\"; echo \"\"; fi; done | less'\n  get-all-resources-in-current-namespace:\n    shortCut: m\n    confirm: false\n    description: List all resources of the current namespace\n    scopes:\n      - configmaps\n      - controllerrevisions\n      - daemonsets\n      - deployments\n      - endpoints\n      - endpointslices\n      - events\n      - horizontalpodautoscalers\n      - ingresses\n      - jobs\n      - leases\n      - limitranges\n      - networkpolicies\n      - persistentvolumeclaims\n      - poddisruptionbudgets\n      - pods\n      - replicasets\n      - replicationcontrollers\n      - resourcequotas\n      - rolebindings\n      - roles\n      - secrets\n      - serviceaccounts\n      - services\n      - statefulsets\n    command: sh\n    background: false\n    args:\n      - -c\n      - 'for r in $(kubectl api-resources --verbs=list --namespaced -o name); do out=$(kubectl get --ignore-not-found --show-kind -n $NAMESPACE $r 2>/dev/null); if [ -n \"$out\" ]; then echo \"$out\"; echo \"\"; fi; done | less'"
  },
  {
    "path": "plugins/get-all.yaml",
    "content": "plugins:\n  #get all resources in a namespace using the krew get-all plugin\n  get-all-namespace:\n    shortCut: g\n    confirm: false\n    description: get-all\n    scopes:\n      - namespaces\n    command: sh\n    background: false\n    args:\n      - -c\n      - \"kubectl get-all --context $CONTEXT -n $NAME | less -K\"\n  get-all-other:\n    shortCut: g\n    confirm: false\n    description: get-all\n    scopes:\n      - all\n    command: sh\n    background: false\n    args:\n      - -c\n      - \"kubectl get-all --context $CONTEXT -n $NAMESPACE | less -K\"\n"
  },
  {
    "path": "plugins/helm-default-values.yaml",
    "content": "plugins:\n  helm-default-values:\n    shortCut: Shift-V\n    confirm: false\n    description: Chart Default Values\n    scopes:\n      - helm\n    command: sh\n    background: false\n    args:\n      - -c\n      - >-\n        revision=$(helm history -n $NAMESPACE --kube-context $CONTEXT $COL-NAME | grep deployed | cut -d$'\\t' -f1 | tr -d ' \\t');\n        kubectl\n        get secrets\n        --context $CONTEXT\n        -n $NAMESPACE\n        sh.helm.release.v1.$COL-NAME.v$revision -o yaml\n        | yq e '.data.release' -\n        | base64 -d\n        | base64 -d\n        | gunzip\n        | jq -r '.chart.values'\n        | yq -P\n        | less -K\n"
  },
  {
    "path": "plugins/helm-diff.yaml",
    "content": "# Requires helm-diff plugin installed: https://github.com/databus23/helm-diff\n# In helm view: <Shift-D> Diff with Previous Revision\n# In helm-history view: <Shift-Q> Diff with Current Revision\nplugins:\n  helm-diff-previous:\n    shortCut: Shift-D\n    confirm: false\n    description: Diff with Previous Revision\n    scopes:\n      - helm\n    command: bash\n    background: false\n    args:\n      - -c\n      - >-\n        LAST_REVISION=$(($COL-REVISION-1));\n        helm diff revision $COL-NAME $COL-REVISION $LAST_REVISION --kube-context $CONTEXT --namespace $NAMESPACE --color | less -RK\n  helm-diff-current:\n    shortCut: Shift-Q\n    confirm: false\n    description: Diff with Current Revision\n    scopes:\n      - history\n    command: bash\n    background: false\n    args:\n      - -c\n      - >-\n        RELEASE_NAME=$(echo $NAME | cut -d':' -f1);\n        LATEST_REVISION=$(helm history -n $NAMESPACE --kube-context $CONTEXT $RELEASE_NAME | grep deployed | cut -d$'\\t' -f1 | tr -d ' \\t');\n        helm diff revision $RELEASE_NAME $LATEST_REVISION $COL-REVISION --kube-context $CONTEXT --namespace $NAMESPACE --color | less -RK"
  },
  {
    "path": "plugins/helm-purge.yaml",
    "content": "# $HOME/.k9s/plugin.yml\nplugins:\n  # Issues a helm delete --purge for the resource associated with the selected pod\n  # Requires https://github.com/derailed/k9s/blob/master/plugins/kubectl/kubectl-purge\n  helm-purge:\n    shortCut: Ctrl-P\n    description: Helm Purge\n    dangerous: true\n    scopes:\n    - po\n    command: kubectl\n    background: true\n    args:\n    - purge\n    - $NAMESPACE\n    - $NAME\n"
  },
  {
    "path": "plugins/helm-values.yaml",
    "content": "# View user-supplied values when the helm chart was created\n\nplugins:\n  helm-values:\n    shortCut: v\n    confirm: false\n    description: Values\n    scopes:\n      - helm\n    command: sh\n    background: false\n    args:\n      - -c\n      - \"helm get values $COL-NAME -n $NAMESPACE --kube-context $CONTEXT | less -K\"\n"
  },
  {
    "path": "plugins/job-suspend.yaml",
    "content": "plugins:\n  # Suspends/Resumes a cronjob\n  toggleCronjob:\n    shortCut: Ctrl-S\n    confirm: true\n    dangerous: true\n    scopes:\n      - cj\n    description: Toggle to suspend or resume a running cronjob\n    command: kubectl\n    background: true\n    args:\n      - patch\n      - cronjobs\n      - $NAME\n      - -n\n      - $NAMESPACE\n      - --context\n      - $CONTEXT\n      - -p\n      - '{\"spec\" : {\"suspend\" : $!COL-SUSPEND }}'\n"
  },
  {
    "path": "plugins/k3d-root-shell.yaml",
    "content": "plugins:\n  # Opens a shell to k3d container as root\n  k3d-root-shell:\n    shortCut: Shift-S\n    confirm: false\n    dangerous: true\n    description: \"Root Shell\"\n    scopes:\n      - containers\n    command: bash\n    background: false\n    args:\n      - -c\n      - |\n        # Check dependencies\n        command -v jq >/dev/null || { echo -e \"jq is not installed (https://stedolan.github.io/jq/)\\nPress 'q' to close\" | less; exit 1; }\n        # Extract node name and container ID\n        POD_DATA=\"$(kubectl get pod/$POD -o json --namespace $NAMESPACE --context $CONTEXT)\"\n        # ${...} is used to prevent variable substitution by k9s (e.g. $POD_DATA)\n        NODE_NAME=$(echo \"${POD_DATA}\" | jq -r '.spec.nodeName')\n        CONTAINER_ID=$(echo \"${POD_DATA}\" | jq -r '.status.containerStatuses[] | select(.name == \"$COL-NAME\") | .containerID ' | grep -oP '(?<=containerd://).*')\n        echo \"<<K9s-Root-Shell>> Pod: $NAMESPACE/$POD | Container: $COL-NAME (${NODE_NAME}/${CONTAINER_ID})\"\n        # Credits for this approach to https://gist.github.com/mamiu/4944e10305bc1c3af84946b33237b0e9\n        docker exec -it $NODE_NAME sh -c \"runc --root /run/containerd/runc/k8s.io/ exec -t -u 0 ${CONTAINER_ID} sh\"\n"
  },
  {
    "path": "plugins/keda-toggle.yaml",
    "content": "plugins:\n  toggle-keda:\n    shortCut: Ctrl-N\n    override: false\n    overwriteOutput: true\n    confirm: false\n    dangerous: true\n    description: Toggle autoscaling on keda scaledobject\n    scopes:\n    - scaledobjects\n    command: bash\n    background: true\n    args:\n    - -c\n    - |\n      ANNOTATION=\"autoscaling.keda.sh/paused-replicas\"\n\n      if kubectl get scaledobject $NAME -n $NAMESPACE --context $CONTEXT -o yaml | grep -q \"$ANNOTATION: \\\"0\\\"\"; then\n        # If annotation found, remove it\n        kubectl annotate scaledobject $NAME \"$ANNOTATION\"- -n $NAMESPACE --context $CONTEXT >/dev/null && echo \"Keda autoscaling for $NAME enabled\"\n      else\n        # If annotation not found, add it\n        kubectl annotate scaledobject $NAME \"$ANNOTATION\"=0 -n $NAMESPACE --context $CONTEXT >/dev/null && echo \"Keda autoscaling for $NAME disabled\"\n      fi\n\n\n"
  },
  {
    "path": "plugins/kube-metrics.yaml",
    "content": "# requires 'kube-metrics' cli binary installed to be installed (https://github.com/bakito/kube-metrics)\nplugins:\n  # allows visualizing pod and node metrics \n  kube-metrics-pod:\n    shortCut: m\n    confirm: false\n    description: \"Metrics\"\n    scopes:\n      - pods\n      - nodes\n    command: sh\n    background: false\n    args:\n      - -c\n      - |\n        if [ -n \"$NAMESPACE\" ]; then\n          kube-metrics pod --namespace=$NAMESPACE $NAME\n        else\n          kube-metrics node $NAME\n        fi\n"
  },
  {
    "path": "plugins/kubectl/kubectl-purge",
    "content": "#!/bin/bash\nusage=\"kubectl $(basename \"$0\") [-h] NAMESPACE NAME\nkubectl plugin, requires TILLER_NS set before call. Will run helm delete --purge on release associated with pod.\nRelease is acquired from the pod's 'describe' information in the 'tags' section.\nExamples:\n  kubectl purge my-namespace my-namespace-pod1-123: Purge the release associated with 'my-namespace-pod1-123' pod\"\nwhile getopts ':h' option; do\n  case \"$option\" in\n    h) echo \"$usage\"\n       exit\n       ;;\n  esac\ndone\nshift $((OPTIND -1))\n\nnamespace=$1\nname=$2\nif [ -z \"$TILLER_NS\" ]; then\n  echo \"Set TILLER_NS environment variable before calling this function\"\n  exit 1;\nelif [ -z \"$namespace\" ]; then\n  echo \"No Namespace provided\"\n  exit 1;\nelif [ -z \"$name\" ]; then\n  echo \"No Name provided\"\n  exit 1;\nfi\n\nkubectl describe pods -n $namespace $name | grep release | cut -f 2 -d'=' | xargs -J rel helm --tiller-namespace $TILLER_NS delete --purge rel\n"
  },
  {
    "path": "plugins/kubectl-get-in-shell.yaml",
    "content": "plugins:\n  # provides a way to continue working on the currently selected object in a new shell without doing lengthy copy/paste of current context.\n  # It simply formats the `kubectl get` command, taking care to omit -n when the namespace is not defined (typically for cluster-wide resources)\n  kubectl-get-cmd:\n    shortCut: Shift-B\n    confirm: false\n    description: get into shell\n    scopes:\n      - all\n    command: bash\n    background: false\n    args:\n      - -c\n      - (printf \"copy/paste in a shell:\\n\\n\"; if [ \"$NAMESPACE\" != \"\" -a  \"$NAMESPACE\" != \"-\"  ]; then printf \"kubectl get  --context $CONTEXT -n $NAMESPACE $RESOURCE_NAME $NAME \\n\" ; else printf \"kubectl get  --context $CONTEXT $RESOURCE_NAME $NAME \\n\"; fi ) |& less\n\n\n"
  },
  {
    "path": "plugins/kubectl-plugins/kubectl-jq",
    "content": "#!/bin/bash\n\nkubectl logs -f $1 -n $2 --context $3 | jq -rR '. as $raw | try (fromjson | .message) catch (\"\\u001b[31m\" + $raw + \"\\u001b[0m\")'\n"
  },
  {
    "path": "plugins/liveMigration.yaml",
    "content": "# $XDG_CONFIG_HOME/k9s/plugins.yaml\nplugins:\n  # liveMigration plugin config by rabin-io\n  #\n  # Trigger virtual machine live migration, for VM's running on k8s cluster using kubevirt\n  #  or Openshift with CNV (OpenShift Virtualization) installed.\n  #\n  # Require `virtctl` cli in your PATH,\n  #   can be downloaded from Openshift `Command Line Tools` page\n  #   or from kubevirt site https://kubevirt.io/user-guide/operations/virtctl_client_tool/\n  #\n  #\n  liveMigration:\n    # Can be triggered from the VMI (VirtualMachineInstance) view, with shortcut `m`\n    shortCut: m\n    # Description to show in K9s menu\n    description: Live Migrate moves VM to another compute node\n    # Enable confirmation dialog\n    confirm: true\n    dangerous: true\n    # Collections of views that support this shortcut. (You can use `all`)\n    scopes:\n    - virtualmachineinstance\n    # Whether or not to run the command in background mode\n    background: false\n    # The command to run upon invocation.\n    command: virtctl\n    # Defines the command arguments\n    args:\n    - migrate\n    - $NAME\n    - -n\n    - $NAMESPACE\n    - --context\n    - $CONTEXT\n"
  },
  {
    "path": "plugins/log-bunyan.yaml",
    "content": "# Forwards logs to bunyan cli for formatting\n# Install Bunyan: https://www.npmjs.com/package/bunyan\nplugins:\n  bunyanlogsp:\n    shortCut: Ctrl-L\n    confirm: false\n    description: \"Logs (bunyan)\"\n    scopes:\n      - pod\n    command: bash\n    background: false\n    args:\n      - -ic\n      - | \n        kubectl logs -f $NAME -n $NAMESPACE --context $CONTEXT | bunyan -o short\n        exit 0 \n  bunyanlogsd:\n    shortCut: Ctrl-L\n    confirm: false\n    description: \"Logs (bunyan)\"\n    scopes:\n      - deployment\n    command: bash\n    background: false\n    args:\n      - -ic\n      - |\n        kubectl logs -f deployment/$NAME -n $NAMESPACE --context $CONTEXT | bunyan -o short\n        exit 0\n  bunyanlogss:\n    shortCut: Ctrl-L\n    confirm: false\n    description: \"Logs (bunyan)\"\n    scopes:\n      - service\n    command: bash\n    background: false\n    args:\n      - -ic\n      - |\n        kubectl logs -f service/$NAME -n $NAMESPACE --context $CONTEXT | bunyan -o short\n        exit 0\n"
  },
  {
    "path": "plugins/log-full.yaml",
    "content": "plugins:\n  # See https://k9scli.io/topics/plugins/\n  raw-logs-follow:\n    shortCut: Ctrl-G\n    description: logs -f\n    scopes:\n    - po\n    command: kubectl\n    background: false\n    args:\n    - logs\n    - -f\n    - $NAME\n    - -n\n    - $NAMESPACE\n    - --context\n    - $CONTEXT\n    - --kubeconfig\n    - $KUBECONFIG\n  log-less:\n    shortCut: Shift-K\n    description: \"logs|less\"\n    scopes:\n    - po\n    command: bash\n    background: false\n    args:\n    - -c\n    - '\"$@\" | less'\n    - dummy-arg\n    - kubectl\n    - logs\n    - $NAME\n    - -n\n    - $NAMESPACE\n    - --context\n    - $CONTEXT\n    - --kubeconfig\n    - $KUBECONFIG\n  log-less-container:\n    shortCut: Shift-L\n    description: \"logs|less\"\n    scopes:\n    - containers\n    command: bash\n    background: false\n    args:\n    - -c\n    - '\"$@\" | less'\n    - dummy-arg\n    - kubectl\n    - logs\n    - -c\n    - $NAME\n    - $POD\n    - -n\n    - $NAMESPACE\n    - --context\n    - $CONTEXT\n    - --kubeconfig\n    - $KUBECONFIG\n"
  },
  {
    "path": "plugins/log-jq.yaml",
    "content": "plugins:\n  # Sends logs over to jq for processing. This leverages kubectl plugin kubectl-jq.\n  jqlogs:\n    shortCut: Ctrl-J\n    confirm: false\n    description: \"Logs (jq)\"\n    scopes:\n      - po\n    command: kubectl\n    background: false\n    args:\n      - jq\n      - $NAME\n      - $NAMESPACE\n      - $CONTEXT\n"
  },
  {
    "path": "plugins/log-loki.yaml",
    "content": "plugins:\n  # https://grafana.com/docs/loki/latest/query/logcli/\n  # you must set the LOKI_ADDR environment variable (\"export LOKI_ADDR=https://loki.internal\" in bash) before starting k9s to use logcli\n  loki-container:\n    shortCut: Shift-L\n    description: \"loki fmt\"\n    scopes:\n    - containers\n    command: logcli\n    background: false\n    args:\n    - query\n    - \"{ namespace = \\\"$NAMESPACE\\\", pod = \\\"$POD\\\", container = \\\"$NAME\\\" }\"\n    - -f\n  loki-container-raw:\n    shortCut: Ctrl-E\n    description: \"loki raw\"\n    scopes:\n    - containers\n    command: logcli\n    background: false\n    args:\n    - query\n    - \"{ namespace = \\\"$NAMESPACE\\\", pod = \\\"$POD\\\", container = \\\"$NAME\\\" }\"\n    - -f\n    - -oraw\n  loki-pods:\n    shortCut: Shift-L\n    description: \"loki fmt\"\n    scopes:\n    - po\n    command: logcli\n    background: false\n    args:\n    - query\n    - \"{ namespace = \\\"$NAMESPACE\\\", pod = \\\"$NAME\\\" }\"\n    - -f\n  loki-pods-raw:\n    shortCut: Ctrl-L\n    description: \"loki raw\"\n    scopes:\n    - po\n    command: logcli\n    background: false\n    args:\n    - query\n    - \"{ namespace = \\\"$NAMESPACE\\\", pod = \\\"$NAME\\\" }\"\n    - -f\n    - -oraw\n  loki-node:\n    shortCut: Shift-L\n    description: \"loki fmt\"\n    scopes:\n    - node\n    command: logcli\n    background: false\n    args:\n    - query\n    - \"{ node_name = \\\"$NAME\\\" }\"\n    - -f\n  loki-node-raw:\n    shortCut: Ctrl-L\n    description: \"loki raw\"\n    scopes:\n    - node\n    command: logcli\n    background: false\n    args:\n    - query\n    - \"{ node_name = \\\"$NAME\\\" }\"\n    - -f\n    - -oraw\n  loki-ns:\n    shortCut: Shift-L\n    description: \"loki fmt\"\n    scopes:\n    - namespace\n    command: logcli\n    background: false\n    args:\n    - query\n    - \"{ namespace = \\\"$NAME\\\" }\"\n    - -f\n  loki-ns-raw:\n    shortCut: Ctrl-L\n    description: \"loki raw\"\n    scopes:\n    - namespace\n    command: logcli\n    background: false\n    args:\n    - query\n    - \"{ namespace = \\\"$NAME\\\" }\"\n    - -f\n    - -oraw\n"
  },
  {
    "path": "plugins/log-stern.yaml",
    "content": "plugins:\n  # Leverage stern (https://github.com/stern/stern) to output logs.\n  stern:\n    shortCut: Ctrl-Y\n    confirm: false\n    description: \"Logs <Stern>\"\n    scopes:\n      - pods\n    command: stern\n    background: false\n    args:\n      - --tail\n      - 50\n      - $FILTER\n      - -n\n      - $NAMESPACE\n      - --context\n      - $CONTEXT\n"
  },
  {
    "path": "plugins/node-root-shell.yaml",
    "content": "plugins:\n  node-root-shell:\n    shortCut: a\n    description: Run root shell on node\n    dangerous: true\n    scopes:\n      - nodes\n    command: bash\n    background: false\n    confirm: true\n    args:\n      - -c\n      - |\n        host=\"$1\"\n        json='\n        {\n          \"apiVersion\": \"v1\",\n          \"spec\": {\n            \"hostIPC\": true,\n            \"hostNetwork\": true,\n            \"hostPID\": true\n        '\n        if ! [[ -z \"$host\" ]]; then\n          json+=\",\n          \\\"nodeSelector\\\" : {\n            \\\"kubernetes.io/hostname\\\" : \\\"$host\\\"\n          }\n          \";\n        fi\n        json+='\n          }\n        }\n        '\n        kubectl run -ti --image alpine:3.8 --rm --privileged --restart=Never --overrides=\"$json\" root --command -- nsenter -t 1 -m -u -n -i -- bash -l\n"
  },
  {
    "path": "plugins/openssl.yaml",
    "content": "# Inspect certificate chains with openssl.\n# See: https://github.com/openssl/openssl.\nplugins:\n  secret-openssl-ca:\n    shortCut: Ctrl-O\n    confirm: false\n    description: Openssl ca.crt\n    scopes:\n      - secrets\n    command: bash\n    background: false\n    args:\n      - -c\n      - kubectl get secret --context $CONTEXT -n $NAMESPACE $NAME -o jsonpath='{.data.ca\\.crt}' | base64 -d | openssl storeutl -noout -text -certs /dev/stdin |& less\n  secret-openssl-tls:\n    shortCut: Shift-O\n    confirm: false\n    description: Openssl tls.crt\n    scopes:\n      - secrets\n    command: bash\n    background: false\n    args:\n      - -c\n      - kubectl get secret --context $CONTEXT -n $NAMESPACE $NAME -o jsonpath='{.data.tls\\.crt}' | base64 -d | openssl storeutl -noout -text -certs /dev/stdin |& less"
  },
  {
    "path": "plugins/pvc-debug-container.yaml",
    "content": "plugins:\n  pvc-shell:\n      shortCut: s\n      description: \"Shell on PVC\"\n      scopes:\n        - pvc\n      command: sh\n      background: false\n      confirm: false\n      inputs:\n        - name: podname\n          label: POD name\n          type: string\n          required: true\n          default: \"pvc-shell\"\n        - name: image\n          label: Image\n          type: dropdown\n          required: true\n          default: nicolaka/netshoot:v0.15\n          options:\n            - nicolaka/netshoot:v0.15\n            - ubuntu:26.04\n        - name: mountpath\n          label: Mount path\n          type: string\n          required: true\n          default: \"/mnt/data\"\n      args:\n        - -c\n        - |\n          NODE=$(kubectl --context $CONTEXT -n $NAMESPACE get pods \\\n            -o jsonpath='{range .items[?(@.spec.volumes[*].persistentVolumeClaim.claimName==\"'\"$NAME\"'\")]}{.spec.nodeName}{\"\\n\"}{end}' | head -n1)\n\n          if [ -n \"$NODE\" ]; then\n            NODE_LINE=\"nodeName: $NODE\"\n          else\n            NODE_LINE=\"\"\n          fi\n\n          echo \"Starting a shell pod with PVC - $NAME mounted at $INPUT_MOUNTPATH\"\n\n          {\n          cat <<EOF\n          apiVersion: v1\n          kind: Pod\n          metadata:\n            name: $INPUT_PODNAME\n            namespace: $NAMESPACE\n          spec:\n            $NODE_LINE\n            restartPolicy: Never\n            tolerations:\n              - operator: Exists\n            containers:\n              - name: shell\n                image: $INPUT_IMAGE\n                command: [\"sh\"]\n                stdin: true\n                tty: true\n                volumeMounts:\n                  - name: vol\n                    mountPath: $INPUT_MOUNTPATH\n            volumes:\n              - name: vol\n                persistentVolumeClaim:\n                  claimName: $NAME\n          EOF\n          } | kubectl --context $CONTEXT apply -f - >/dev/null 2>&1\n\n          echo \"Waiting for pod to be ready.\"\n          if ! kubectl --context $CONTEXT -n $NAMESPACE wait --for=condition=Ready pod/$INPUT_PODNAME --timeout=60s; then\n            echo \"Pod did not become Ready. Likely a ReadWriteOnce conflict.\"\n            echo \"Press Enter to return to k9s.\"\n            read dummy\n            kubectl --context $CONTEXT -n $NAMESPACE delete pod $INPUT_PODNAME --ignore-not-found --grace-period=0 --force >/dev/null 2>&1\n            exit 0\n          fi\n\n          kubectl --context $CONTEXT -n $NAMESPACE exec -it $INPUT_PODNAME -- bash || echo \"Could not exec into pod.\"\n\n          echo \"Cleaning up pod.\"\n          kubectl --context $CONTEXT -n $NAMESPACE delete pod $INPUT_PODNAME --ignore-not-found --grace-period=0 --force >/dev/null 2>&1\n"
  },
  {
    "path": "plugins/remove-finalizers.yaml",
    "content": "# Removes all finalizers from the selected resource. Finalizers are namespaced keys that tell Kubernetes to wait\n# until specific conditions are met before it fully deletes resources marked for deletion.\n# Before deleting an object you need to ensure that all finalizers has been removed. Usually this would be done\n# by the specific controller but under some circumstances it is possible to encounter a set of objects blocked\n# for deletion.\n# This plugin makes this task easier by providing a shortcut to directly removing them all.\n# Be careful when using this plugin as it may leave dangling resources or instantly deleting resources that were\n# blocked by the finalizers.\n# Author: github.com/jalvarezit\nplugins:\n  remove_finalizers:\n    shortCut: Ctrl-F\n    confirm: true\n    dangerous: true\n    scopes:\n      - all\n    description: |\n      Removes all finalizers from selected resource. Be careful when using it,\n      it may leave dangling resources or delete them\n    command: kubectl\n    background: true\n    args:\n      - patch\n      - --context\n      - $CONTEXT\n      - --namespace\n      - $NAMESPACE\n      - $RESOURCE_NAME.$RESOURCE_GROUP\n      - $NAME\n      - -p\n      - '{\"metadata\":{\"finalizers\":null}}'\n      - --type\n      - merge\n"
  },
  {
    "path": "plugins/resource-recommendations.yaml",
    "content": "plugins:\n# Author: Daniel Rubin\n# Get recommendations for CPU/Memory requests and limits using Robusta KRR\n# Requires Prometheus in the Cluster and Robusta KRR (https://github.com/robusta-dev/krr) on your system\n# Open K9s in deployments/daemonsets/statefulsets view, then:\n# Shift-K to get recommendations\n  krr:\n    shortCut: Shift-K\n    description: Get krr\n    scopes:\n      - deployments\n      - daemonsets\n      - statefulsets\n      - cronjobs\n    command: bash\n    background: false\n    confirm: false\n    args:\n      - -c\n      - |\n        LABELS=$(kubectl get $RESOURCE_NAME $NAME -n $NAMESPACE  --context $CONTEXT  --show-labels | awk '{print $NF}' | awk '{if(NR>1)print}')\n        krr simple --cluster $CONTEXT --selector $LABELS \n        echo \"Press 'q' to exit\"\n        while : ; do\n        read -n 1 k <&1\n        if [[ $k = q ]] ; then\n        break\n        fi\n        done\n  krr-ns:\n    shortCut: Shift-K\n    description: Get krr\n    scopes:\n      - namespaces\n    command: bash\n    background: false\n    confirm: false\n    args:\n      - -c\n      - |\n        krr simple --cluster $CONTEXT -n $RESOURCE_NAME\n        echo \"Press 'q' to exit\"\n        while : ; do\n        read -n 1 k <&1\n        if [[ $k = q ]] ; then\n        break\n        fi\n        done"
  },
  {
    "path": "plugins/rm-ns.yaml",
    "content": "plugins:\n  # remove finalizers from a stuck namespace\n  rm-ns:\n    shortCut: n\n    confirm: true\n    dangerous: true\n    description: Remove NS Finalizers\n    scopes:\n    - namespace\n    command: sh\n    background: false\n    args:\n    - -c\n    - \"kubectl get namespace $NAME -o json | jq '.spec.finalizers=[]' | kubectl replace --raw /api/v1/namespaces/$NAME/finalize -f - > /dev/null\"\n"
  },
  {
    "path": "plugins/spark-operator.yaml",
    "content": "# See https://github.com/kubeflow/spark-operator\nplugins:\n  toggleScheduledSparkApp:\n    shortCut: s\n    confirm: true\n    dangerous: true\n    scopes:\n      - scheduledsparkapp\n    description: Toggle suspend\n    command: kubectl\n    background: true\n    args:\n      - patch\n      - scheduledsparkapp\n      - $NAME\n      - -n\n      - $NAMESPACE\n      - --context\n      - $CONTEXT\n      - -p\n      - '{\"spec\": {\"suspend\": $!COL-SUSPEND}}'\n      - --type\n      - merge\n"
  },
  {
    "path": "plugins/start-alpine.yaml",
    "content": "plugins:\n  start-alpine-pod:\n    shortCut: Ctrl-T\n    confirm: true\n    description: \"Start an alpine:latest pod in current context/namespace\"\n    scopes:\n      - pods\n      - deployments\n    command: bash\n    background: true\n    args:\n      - -c\n      - |\n        echo '{\"apiVersion\": \"apps/v1\", \"kind\": \"Deployment\", \"metadata\": {\"name\": \"alpine\", \"labels\": {\"app\": \"alpine\", \"debug\": \"1\"}}, \"spec\": {\"selector\": {\"matchLabels\": {\"app\": \"alpine\"}}, \"replicas\": 1, \"template\": {\"metadata\": {\"labels\": {\"app\": \"alpine\", \"debug\": \"1\"}, \"annotations\": {\"kubectl.kubernetes.io/default-container\": \"alpine\"}}, \"spec\": {\"containers\": [{\"name\": \"alpine\", \"image\": \"alpine:latest\", \"imagePullPolicy\": \"Always\", \"securityContext\": {\"runAsUser\": 0, \"runAsGroup\": 0}, \"stdin\": true, \"tty\": true, \"stdinOnce\": true, \"terminationMessagePath\": \"/dev/termination-log\", \"terminationMessagePolicy\": \"File\", \"resources\": {\"requests\": {\"cpu\": \"100m\", \"memory\": \"100Mi\"}, \"limits\": {\"cpu\": \"100m\", \"memory\": \"100Mi\"}}}], \"restartPolicy\": \"Always\"}}}}' | kubectl apply -f - --context $CONTEXT --namespace $NAMESPACE\n"
  },
  {
    "path": "plugins/szero.yaml",
    "content": "# Temporarily scale down/up all deployments, statefulsets, and daemonsets in a namespace using szero\n# Uses https://github.com/jadolg/szero\n\nplugins:\n  szero-down:\n    shortCut: Shift-D\n    confirm: true\n    dangerous: true\n    description: Scale all down\n    scopes:\n      - namespace\n    command: sh\n    background: false\n    args:\n      - -c\n      - \"szero down --context $CONTEXT --namespace $NAME\"\n  szero-up:\n    shortCut: Shift-U\n    confirm: true\n    dangerous: true\n    description: Scale all up\n    scopes:\n      - namespace\n    command: sh\n    background: false\n    args:\n      - -c\n      - \"szero up --context $CONTEXT --namespace $NAME\"\n"
  },
  {
    "path": "plugins/trace-dns.yaml",
    "content": "# Author: Qasim Sarfraz\n# Trace DNS requests for containers, pods, and nodes\n# Requires kubectl version 1.30 or later\n# https://github.com/inspektor-gadget/inspektor-gadget\n# https://www.inspektor-gadget.io/docs/latest/gadgets/trace_dns\nplugins:\n  trace-dns:\n    shortCut: Shift-D\n    description: Trace DNS requests\n    scopes:\n      - containers\n      - pods\n      - nodes\n    command: bash\n    confirm: false\n    background: false\n    args:\n      - -c\n      - |\n        IG_VERSION=v0.34.0\n        IG_IMAGE=ghcr.io/inspektor-gadget/ig:$IG_VERSION\n        IG_FIELD=k8s.podName,src,dst,qr,qtype,name,rcode,latency_ns\n        \n        GREEN='\\033[0;32m'\n        RED='\\033[0;31m'\n        BLUE='\\033[0;34m'\n        NC='\\033[0m' # No Color\n        \n        # Ensure kubectl version is 1.30 or later\n        KUBECTL_VERSION=$(kubectl version --client | awk '/Client Version:/{print $3}')\n        if [[ \"$(echo \"$KUBECTL_VERSION\" | cut -d. -f2)\" -lt 30 ]]; then\n          echo -e \"${RED}kubectl version 1.30 or later is required${NC}\"\n          sleep 3\n          exit\n        fi\n        \n        clear\n\n        # Handle containers\n        if [[ -n \"$POD\" ]]; then\n          echo -e \"${GREEN}Tracing DNS requests for container ${BLUE}${NAME}${GREEN} in pod ${BLUE}${POD}${GREEN} in namespace ${BLUE}${NAMESPACE}${NC}\"\n          IG_NODE=$(kubectl get pod \"$POD\" -n \"$NAMESPACE\" -o jsonpath='{.spec.nodeName}')\n          kubectl debug --kubeconfig=$KUBECONFIG  --context=$CONTEXT -q \\\n            --profile=sysadmin \"node/$IG_NODE\" -it --image=\"$IG_IMAGE\" -- \\\n            ig run trace_dns:$IG_VERSION -F \"k8s.podName==$POD\" -F \"k8s.containerName=$NAME\" \\\n            --fields \"$IG_FIELD\"\n            exit\n        fi\n        \n        # Handle pods\n        if [[ -n \"$NAMESPACE\" ]]; then\n          echo -e \"${GREEN}Tracing DNS requests for pod ${BLUE}${NAME}${GREEN} in namespace ${BLUE}${NAMESPACE}${NC}\"\n          IG_NODE=$(kubectl get pod \"$NAME\" -n \"$NAMESPACE\" -o jsonpath='{.spec.nodeName}')\n          kubectl debug --kubeconfig=$KUBECONFIG  --context=$CONTEXT -q \\\n            --profile=sysadmin  -it --image=\"$IG_IMAGE\" \"node/$IG_NODE\" -- \\\n            ig run trace_dns:$IG_VERSION -F \"k8s.podName==$NAME\" \\\n            --fields \"$IG_FIELD\"\n            exit\n        fi\n        \n        # Handle nodes\n        echo -e \"${GREEN}Tracing DNS requests for node ${BLUE}${NAME}${NC}\"\n        kubectl debug --kubeconfig=$KUBECONFIG  --context=$CONTEXT -q \\\n          --profile=sysadmin -it --image=\"$IG_IMAGE\" \"node/$NAME\" -- \\\n          ig run trace_dns:$IG_VERSION --fields \"$IG_FIELD\"\n"
  },
  {
    "path": "plugins/vector-dev-top.yaml",
    "content": "# SPDX-FileCopyrightText: 2025 Robin Schneider <ypid@riseup.net>\n#\n# SPDX-License-Identifier: CC0-1.0\n\nplugins:\n  vector-top:\n    # t hotkey is already used for \"transfer\" by k9s.\n    # Using h because of health.\n    shortCut: h\n    confirm: false\n    description: \"Execute `vector top`\"\n    scopes:\n      - pods\n    command: sh\n    background: false\n    args:\n      # Both works. I have not read through https://github.com/derailed/k9s/issues/1852 yet.\n      # - -ic\n      - -c\n      - \"kubectl exec --context=$CONTEXT --namespace=$NAMESPACE --stdin --tty $NAME -- vector top\"\n\n  vector-top-container:\n    # t hotkey is already used for \"transfer\" by k9s.\n    shortCut: h\n    confirm: false\n    description: \"Execute `vector top`\"\n    scopes:\n      - containers\n    command: sh\n    background: false\n    args:\n      - -c\n      - \"kubectl exec --context=$CONTEXT --namespace=$NAMESPACE --stdin --tty $POD --container=$NAME -- vector top\"\n"
  },
  {
    "path": "plugins/watch-events.yaml",
    "content": "# watch events on selected resources\n# requires linux \"watch\" command\n# change '-n' to adjust refresh time in seconds\nplugins:\n  watch-events:\n    shortCut: Shift-E\n    confirm: false\n    description: Get Events\n    scopes:\n    - all\n    command: sh\n    background: false\n    args:\n    - -c\n    - \"kubectl events --context $CONTEXT --namespace $NAMESPACE --for $RESOURCE_NAME.${RESOURCE_GROUP:+.RESOURCE_GROUP}/$NAME --watch\"\n"
  },
  {
    "path": "skins/axual.yaml",
    "content": "# -----------------------------------------------------------------------------\n# Axual Skin contributed by [@JayKus](jimmy@axual.com)\n# -----------------------------------------------------------------------------\n\n# Styles...\nblue: &blue \"#113851\"\nred: &red \"#D7595F\"\nyellow: &yellow \"#F2BF40\"\ncyan: &cyan \"#47B0AB\"\ngrey: &grey \"#A99688\"\nlight: &light \"#F1EFE4\"\n\n# Skin...\nk9s:\n  # General K9s styles\n  body:\n    fgColor: *yellow\n    bgColor: *blue\n    logoColor: orange\n\n  # Command prompt styles\n  prompt:\n    fgColor: *yellow\n    bgColor: *blue\n    suggestColor: orange\n\n  # ClusterInfoView styles\n  info:\n    fgColor: *yellow\n    sectionColor: *light\n\n  dialog:\n    fgColor: *yellow\n    bgColor: *blue\n    buttonFgColor: *yellow\n    buttonBgColor: *blue\n    buttonFocusFgColor: *grey\n    buttonFocusBgColor: *light\n    labelFgColor: *grey\n    fieldFgColor: *light\n\n  # Frame styles\n  frame:\n    # Borders styles\n    border:\n      fgColor: *yellow\n      focusColor: *light\n\n    # MenuView attributes and styles\n    menu:\n      fgColor: *yellow\n      keyColor: *yellow\n      # Used for favorite namespaces\n      numKeyColor: *light\n\n    # CrumbView attributes for history navigation.\n    crumbs:\n      fgColor: *light\n      bgColor: *cyan\n      activeColor: *red\n\n    # Resource status and update styles\n    status:\n      # Text display, highlight is same colors inverted\n      newColor: *yellow\n      modifyColor: *blue\n      addColor: *blue\n      errorColor: *red\n      highlightColor: *yellow\n      killColor: *red\n      completedColor: *grey\n\n    # Border title styles.\n    title:\n      fgColor: *yellow\n      bgColor: *blue\n      highlightColor: *cyan\n      counterColor: *light\n      filterColor: \"slategray\"\n  # Specific views styles\n  views:\n    # TableView attributes.\n    table:\n      fgColor: *blue\n      bgColor: *blue\n      cursorColor: *blue\n      # Header row styles.\n      header:\n        fgColor: *light\n        bgColor: *blue\n        sorterColor: \"orange\"\n\n    # Xray style\n    xray:\n      fgColor: *light\n      bgColor: *blue\n      cursorColor: *red\n      graphicColor: *yellow\n      showIcons: false\n\n    # YAML info styles.\n    yaml:\n      keyColor: *yellow\n      colonColor: *yellow\n      valueColor: *cyan\n\n    # Logs styles.\n    logs:\n      fgColor: *yellow\n      bgColor: *blue\n      indicator:\n        fgColor: *red\n        bgColor: *blue\n        toggleOnColor: *yellow\n        toggleOffColor: *grey\n\n    # Chart drawing\n    charts:\n      bgColor: *blue\n      defaultDialColors:\n        - *light\n        - *red\n      defaultChartColors:\n        - *light\n        - *red\n"
  },
  {
    "path": "skins/black-and-wtf.yaml",
    "content": "# -----------------------------------------------------------------------------\n# BlackAndWtf skin\n# -----------------------------------------------------------------------------\n\n# Styles...\nfg: &fg \"white\"\nbg: &bg \"black\"\nmark: &mark \"darkgoldenrod\"\nactive: &active \"dimgray\"\ntext: &text \"navajowhite\"\nwhite: &white \"whitesmoke\"\nghost: &ghost \"ghostwhite\"\ndslate: &dslate \"darkslategray\"\nerr: &err \"pink\"\nslate: &slate \"slategray\"\ngray: &gray \"gray\"\n\n# Skin...\nk9s:\n  body:\n    fgColor: *fg\n    bgColor: *bg\n    logoColor: *fg\n  prompt:\n    fgColor: *fg\n    bgColor: *bg\n    suggestColor: *gray\n  info:\n    fgColor: *text\n    sectionColor: *fg\n  dialog:\n    fgColor: *fg\n    bgColor: *bg\n    buttonFgColor: *fg\n    buttonBgColor: *bg\n    buttonFocusFgColor: *slate\n    buttonFocusBgColor: *white\n    labelFgColor: *ghost\n    fieldFgColor: *white\n  frame:\n    border:\n      fgColor: *fg\n      focusColor: *fg\n    menu:\n      fgColor: *fg\n      keyColor: *fg\n      numKeyColor: *text\n    crumbs:\n      fgColor: *fg\n      bgColor: *bg\n      activeColor: *active\n    status:\n      newColor: *white\n      modifyColor: *text\n      addColor: *ghost\n      errorColor: *err\n      highlightColor: *dslate\n      killColor: *slate\n      completedColor: *gray\n    title:\n      fgColor: *fg\n      highlightColor: *active\n      counterColor: *text\n      filterColor: *slate\n  views:\n    table:\n      fgColor: *fg\n      bgColor: *bg\n      cursorBgColor: *fg\n      cursorFgColor: *bg\n      markColor: *mark\n      header:\n        fgColor: *dslate\n        bgColor: *bg\n        sorterColor: *fg\n    xray:\n      fgColor: *fg\n      bgColor: *bg\n      cursorColor: *ghost\n      graphicColor: *gray\n      showIcons: false\n    yaml:\n      keyColor: *ghost\n      colorColor: *slate\n      valueColor: *text\n    logs:\n      fgColor: *ghost\n      bgColor: *bg\n      indicator:\n        fgColor: *ghost\n        bgColor: *bg\n        toggleOnColor: *mark\n        toggleOffColor: *gray\n    charts:\n      bgColor: default\n      defaultDialColors:\n        - *white\n        - *err\n      defaultChartColors:\n        - *white\n        - *err\n"
  },
  {
    "path": "skins/dracula.yaml",
    "content": "# -----------------------------------------------------------------------------\n# Dracula skin\n# -----------------------------------------------------------------------------\n\n# Styles...\nforeground: &foreground \"#f8f8f2\"\nbackground: &background \"#282a36\"\ncurrent_line: &current_line \"#44475a\"\nselection: &selection \"#44475a\"\ncomment: &comment \"#6272a4\"\ncyan: &cyan \"#8be9fd\"\ngreen: &green \"#50fa7b\"\norange: &orange \"#ffb86c\"\npink: &pink \"#ff79c6\"\npurple: &purple \"#bd93f9\"\nred: &red \"#ff5555\"\nyellow: &yellow \"#f1fa8c\"\n\n# Skin...\nk9s:\n  # General K9s styles\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *purple\n  # Command prompt styles\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *purple\n  # ClusterInfoView styles.\n  info:\n    fgColor: *pink\n    sectionColor: *foreground\n  # Dialog styles.\n  dialog:\n    fgColor: *foreground\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *purple\n    buttonFocusFgColor: *yellow\n    buttonFocusBgColor: *pink\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  frame:\n    # Borders styles.\n    border:\n      fgColor: *selection\n      focusColor: *current_line\n    menu:\n      fgColor: *foreground\n      keyColor: *pink\n      # Used for favorite namespaces\n      numKeyColor: *pink\n    # CrumbView attributes for history navigation.\n    crumbs:\n      fgColor: *foreground\n      bgColor: *current_line\n      activeColor: *current_line\n    # Resource status and update styles\n    status:\n      newColor: *cyan\n      modifyColor: *purple\n      addColor: *green\n      errorColor: *red\n      highlightColor: *orange\n      killColor: *comment\n      completedColor: *comment\n    # Border title styles.\n    title:\n      fgColor: *foreground\n      bgColor: *current_line\n      highlightColor: *orange\n      counterColor: *purple\n      filterColor: *pink\n  views:\n    # Charts skins...\n    charts:\n      bgColor: default\n      defaultDialColors:\n        - *purple\n        - *red\n      defaultChartColors:\n        - *purple\n        - *red\n    # TableView attributes.\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      # Header row styles.\n      header:\n        fgColor: *foreground\n        bgColor: *background\n        sorterColor: *cyan\n    # Xray view attributes.\n    xray:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *current_line\n      graphicColor: *purple\n      showIcons: false\n    # YAML info styles.\n    yaml:\n      keyColor: *pink\n      colonColor: *purple\n      valueColor: *foreground\n    # Logs styles.\n    logs:\n      fgColor: *foreground\n      bgColor: *background\n      indicator:\n        fgColor: *foreground\n        bgColor: *purple\n        toggleOnColor: *green\n        toggleOffColor: *cyan"
  },
  {
    "path": "skins/everforest-dark.yaml",
    "content": "# -----------------------------------------------------------------------------\n# Everforest Dark\n# https://github.com/sainnhe/everforest/blob/master/palette.md#dark\n# -----------------------------------------------------------------------------\n#\ntext: &text \"#d3c6aa\"\nbase: &base \"#1e2326\"\noverlay: &overlay \"#2e383c\"\nmuted: &muted \"#495156\"\nred: &red \"#e67e80\"\nblue: &blue \"#7fbbb3\"\nyellow: &yellow \"#dbbc7f\"\ngreen: &green \"#83c092\"\npink: &pink \"#d699b6\"\norange: &orange \"#e69875\"\n\n# Skin...\nk9s:\n  # General K9s styles\n  body:\n    fgColor: *text\n    bgColor: *base\n    logoColor: *green\n  # Command prompt styles\n  prompt:\n    fgColor: *text\n    bgColor: *base\n    suggestColor: *green\n    border:\n      command: *orange\n      default: *blue\n  # ClusterInfoView styles.\n  info:\n    fgColor: *green\n    sectionColor: *text\n  # Dialog styles.\n  dialog:\n    fgColor: *text\n    bgColor: *base\n    buttonFgColor: *text\n    buttonBgColor: *green\n    buttonFocusFgColor: *yellow\n    buttonFocusBgColor: *green\n    labelFgColor: *yellow\n    fieldFgColor: *text\n  frame:\n    # Borders styles.\n    border:\n      fgColor: *overlay\n      focusColor: *overlay\n    menu:\n      fgColor: *text\n      keyColor: *green\n      # Used for favorite namespaces\n      numKeyColor: *green\n    # CrumbView attributes for history navigation.\n    crumbs:\n      fgColor: *text\n      bgColor: *overlay\n      activeColor: *overlay\n    # Resource status and update styles\n    status:\n      newColor: *green\n      modifyColor: *red\n      addColor: *blue\n      errorColor: *pink\n      highlightcolor: *yellow\n      killColor: *muted\n      completedColor: *muted\n    # Border title styles.\n    title:\n      fgColor: *text\n      bgColor: *overlay\n      highlightColor: *yellow\n      counterColor: *green\n      filterColor: *green\n  views:\n    # Charts skins...\n    charts:\n      bgColor: default\n      defaultDialColors:\n        - *green\n        - *pink\n      defaultChartColors:\n        - *green\n        - *pink\n    # TableView attributes.\n    table:\n      fgColor: *text\n      bgColor: *base\n      # Header row styles.\n      header:\n        fgColor: *text\n        bgColor: *base\n        sorterColor: *red\n    # Xray view attributes.\n    xray:\n      fgColor: *text\n      bgColor: *base\n      cursorColor: *overlay\n      graphicColor: *green\n      showIcons: false\n    # YAML info styles.\n    yaml:\n      keyColor: *green\n      colonColor: *green\n      valueColor: *text\n    # Logs styles.\n    logs:\n      fgColor: *text\n      bgColor: *base\n      indicator:\n        fgColor: *text\n        bgColor: *base\n"
  },
  {
    "path": "skins/everforest-light.yaml",
    "content": "# -----------------------------------------------------------------------------\n# Everforest Light\n# https://github.com/sainnhe/everforest/blob/master/palette.md#dark\n# -----------------------------------------------------------------------------\n#\ntext: &text \"#5c6a72\"\nbase: &base \"#f2efdf\"\noverlay: &overlay \"#fffbef\"\nmuted: &muted \"#edeada\"\nred: &red \"#f85552\"\nblue: &blue \"#3a94c5\"\nyellow: &yellow \"#dfa000\"\ngreen: &green \"#35a77c\"\npink: &pink \"#df69ba\"\norange: &orange \"#f57d26\"\n\n# Skin...\nk9s:\n  # General K9s styles\n  body:\n    fgColor: *text\n    bgColor: *base\n    logoColor: *green\n  # Command prompt styles\n  prompt:\n    fgColor: *text\n    bgColor: *base\n    suggestColor: *green\n    border:\n      command: *orange\n      default: *blue\n  # ClusterInfoView styles.\n  info:\n    fgColor: *green\n    sectionColor: *text\n  # Dialog styles.\n  dialog:\n    fgColor: *text\n    bgColor: *base\n    buttonFgColor: *text\n    buttonBgColor: *green\n    buttonFocusFgColor: *yellow\n    buttonFocusBgColor: *green\n    labelFgColor: *yellow\n    fieldFgColor: *text\n  frame:\n    # Borders styles.\n    border:\n      fgColor: *overlay\n      focusColor: *overlay\n    menu:\n      fgColor: *text\n      keyColor: *green\n      # Used for favorite namespaces\n      numKeyColor: *green\n    # CrumbView attributes for history navigation.\n    crumbs:\n      fgColor: *text\n      bgColor: *overlay\n      activeColor: *overlay\n    # Resource status and update styles\n    status:\n      newColor: *green\n      modifyColor: *red\n      addColor: *blue\n      errorColor: *pink\n      highlightcolor: *yellow\n      killColor: *muted\n      completedColor: *muted\n    # Border title styles.\n    title:\n      fgColor: *text\n      bgColor: *overlay\n      highlightColor: *yellow\n      counterColor: *green\n      filterColor: *green\n  views:\n    # Charts skins...\n    charts:\n      bgColor: default\n      defaultDialColors:\n        - *green\n        - *pink\n      defaultChartColors:\n        - *green\n        - *pink\n    # TableView attributes.\n    table:\n      fgColor: *text\n      bgColor: *base\n      # Header row styles.\n      header:\n        fgColor: *text\n        bgColor: *base\n        sorterColor: *red\n    # Xray view attributes.\n    xray:\n      fgColor: *text\n      bgColor: *base\n      cursorColor: *overlay\n      graphicColor: *green\n      showIcons: false\n    # YAML info styles.\n    yaml:\n      keyColor: *green\n      colonColor: *green\n      valueColor: *text\n    # Logs styles.\n    logs:\n      fgColor: *text\n      bgColor: *base\n      indicator:\n        fgColor: *text\n        bgColor: *base\n"
  },
  {
    "path": "skins/gruvbox-dark-hard.yaml",
    "content": "# -----------------------------------------------------------------------------\n# K9s Gruvbox Dark Skin\n# -----------------------------------------------------------------------------\n\n# Styles...\nforeground: &foreground \"#ebdbb2\"\nbackground: &background \"#1d2021\"\ncurrent_line: &current_line \"#ebdbb2\"\nselection: &selection \"#3c3735\"\ncomment: &comment \"#bdad93\"\ncyan: &cyan \"#689d69\"\ngreen: &green \"#989719\"\norange: &orange \"#d79920\"\nmagenta: &magenta \"#b16185\"\nblue: &blue \"#448488\"\nred: &red \"#cc231c\"\n\nk9s:\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *blue\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *orange\n  info:\n    fgColor: *magenta\n    sectionColor: *foreground\n  help:\n    fgColor: *foreground\n    bgColor: *background\n    keyColor: *magenta\n    numKeyColor: *blue\n    sectionColor: *green\n  dialog:\n    fgColor: *foreground\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *magenta\n    buttonFocusFgColor: white\n    buttonFocusBgColor: *cyan\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  frame:\n    border:\n      fgColor: *selection\n      focusColor: *current_line\n    menu:\n      fgColor: *foreground\n      keyColor: *magenta\n      numKeyColor: *magenta\n    crumbs:\n      fgColor: *foreground\n      bgColor: *comment\n      activeColor: *blue\n    status:\n      newColor: *cyan\n      modifyColor: *blue\n      addColor: *green\n      errorColor: *red\n      highlightColor: *orange\n      killColor: *comment\n      completedColor: *comment\n    title:\n      fgColor: *foreground\n      bgColor: *background\n      highlightColor: *orange\n      counterColor: *blue\n      filterColor: *magenta\n  views:\n    charts:\n      bgColor: background\n      defaultDialColors:\n        - *blue\n        - *red\n      defaultChartColors:\n        - *blue\n        - *red\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      cursorFgColor: \"#fff\"\n      cursorBgColor: *current_line\n      header:\n        fgColor: *foreground\n        bgColor: *background\n        sorterColor: *selection\n    xray:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *current_line\n      graphicColor: *blue\n      showIcons: false\n    yaml:\n      keyColor: *magenta\n      colonColor: *blue\n      valueColor: *foreground\n    logs:\n      fgColor: *foreground\n      bgColor: *background\n      indicator:\n        fgColor: *foreground\n        bgColor: *background\n"
  },
  {
    "path": "skins/gruvbox-dark.yaml",
    "content": "# -----------------------------------------------------------------------------\n# K9s Gruvbox Dark Skin\n# Author: [@indiebrain](https://github.com/indiebrain)\n# -----------------------------------------------------------------------------\n\n# Styles...\nforeground: &foreground \"#ebdbb2\"\nbackground: &background \"#272727\"\ncurrent_line: &current_line \"#ebdbb2\"\nselection: &selection \"#3c3735\"\ncomment: &comment \"#bdad93\"\ncyan: &cyan \"#689d69\"\ngreen: &green \"#989719\"\norange: &orange \"#d79920\"\nmagenta: &magenta \"#b16185\"\nblue: &blue \"#448488\"\nred: &red \"#cc231c\"\n\nk9s:\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *blue\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *orange\n  info:\n    fgColor: *magenta\n    sectionColor: *foreground\n  help:\n    fgColor: *foreground\n    bgColor: *background\n    keyColor: *magenta\n    numKeyColor: *blue\n    sectionColor: *green\n  dialog:\n    fgColor: *foreground\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *magenta\n    buttonFocusFgColor: white\n    buttonFocusBgColor: *cyan\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  frame:\n    border:\n      fgColor: *selection\n      focusColor: *current_line\n    menu:\n      fgColor: *foreground\n      keyColor: *magenta\n      numKeyColor: *magenta\n    crumbs:\n      fgColor: *foreground\n      bgColor: *comment\n      activeColor: *blue\n    status:\n      newColor: *cyan\n      modifyColor: *blue\n      addColor: *green\n      errorColor: *red\n      highlightColor: *orange\n      killColor: *comment\n      completedColor: *comment\n    title:\n      fgColor: *foreground\n      bgColor: *background\n      highlightColor: *orange\n      counterColor: *blue\n      filterColor: *magenta\n  views:\n    charts:\n      bgColor: background\n      defaultDialColors:\n        - *blue\n        - *red\n      defaultChartColors:\n        - *blue\n        - *red\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      cursorFgColor: \"#fff\"\n      cursorBgColor: *current_line\n      header:\n        fgColor: *foreground\n        bgColor: *background\n        sorterColor: *selection\n    xray:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *current_line\n      graphicColor: *blue\n      showIcons: false\n    yaml:\n      keyColor: *magenta\n      colonColor: *blue\n      valueColor: *foreground\n    logs:\n      fgColor: *foreground\n      bgColor: *background\n      indicator:\n        fgColor: *foreground\n        bgColor: *background\n"
  },
  {
    "path": "skins/gruvbox-light-hard.yaml",
    "content": "# -----------------------------------------------------------------------------\n# K9s Gruvbox Light Skin\n# -----------------------------------------------------------------------------\n\n# Styles...\nforeground: &foreground \"#3c3735\"\nbackground: &background \"#f9f5d7\"\ncurrent_line: &current_line \"#ebdbb2\"\nselection: &selection \"#3c3735\"\ncomment: &comment \"#bdad93\"\ncyan: &cyan \"#689d69\"\ngreen: &green \"#989719\"\norange: &orange \"#d79920\"\nmagenta: &magenta \"#b16185\"\nblue: &blue \"#448488\"\nred: &red \"#cc231c\"\n\nk9s:\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *blue\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *orange\n  info:\n    fgColor: *magenta\n    sectionColor: *foreground\n  help:\n    fgColor: *foreground\n    bgColor: *background\n    keyColor: *magenta\n    numKeyColor: *blue\n    sectionColor: *green\n  dialog:\n    fgColor: *foreground\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *magenta\n    buttonFocusFgColor: white\n    buttonFocusBgColor: *cyan\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  frame:\n    border:\n      fgColor: *selection\n      focusColor: *current_line\n    menu:\n      fgColor: *foreground\n      keyColor: *magenta\n      numKeyColor: *magenta\n    crumbs:\n      fgColor: *foreground\n      bgColor: *comment\n      activeColor: *blue\n    status:\n      newColor: *cyan\n      modifyColor: *blue\n      addColor: *green\n      errorColor: *red\n      highlightColor: *orange\n      killColor: *comment\n      completedColor: *comment\n    title:\n      fgColor: *foreground\n      bgColor: *background\n      highlightColor: *orange\n      counterColor: *blue\n      filterColor: *magenta\n  views:\n    charts:\n      bgColor: background\n      defaultDialColors:\n        - *blue\n        - *red\n      defaultChartColors:\n        - *blue\n        - *red\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      cursorFgColor: *foreground\n      cursorBgColor: *current_line\n      header:\n        fgColor: *foreground\n        bgColor: *background\n        sorterColor: *selection\n    xray:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *current_line\n      graphicColor: *blue\n      showIcons: false\n    yaml:\n      keyColor: *magenta\n      colonColor: *blue\n      valueColor: *foreground\n    logs:\n      fgColor: *foreground\n      bgColor: *background\n      indicator:\n        fgColor: *foreground\n        bgColor: *background\n        toggleOnColor: *magenta\n        toggleOffColor: *blue\n"
  },
  {
    "path": "skins/gruvbox-light.yaml",
    "content": "# -----------------------------------------------------------------------------\n# K9s Gruvbox Light Skin\n# Author: [@indiebrain](https://github.com/indiebrain)\n# -----------------------------------------------------------------------------\n\n# Styles...\nforeground: &foreground \"#3c3735\"\nbackground: &background \"#fbf1c7\"\ncurrent_line: &current_line \"#ebdbb2\"\nselection: &selection \"#3c3735\"\ncomment: &comment \"#bdad93\"\ncyan: &cyan \"#689d69\"\ngreen: &green \"#989719\"\norange: &orange \"#d79920\"\nmagenta: &magenta \"#b16185\"\nblue: &blue \"#448488\"\nred: &red \"#cc231c\"\n\nk9s:\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *blue\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *orange\n  info:\n    fgColor: *magenta\n    sectionColor: *foreground\n  help:\n    fgColor: *foreground\n    bgColor: *background\n    keyColor: *magenta\n    numKeyColor: *blue\n    sectionColor: *green\n  dialog:\n    fgColor: *foreground\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *magenta\n    buttonFocusFgColor: white\n    buttonFocusBgColor: *cyan\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  frame:\n    border:\n      fgColor: *selection\n      focusColor: *current_line\n    menu:\n      fgColor: *foreground\n      keyColor: *magenta\n      numKeyColor: *magenta\n    crumbs:\n      fgColor: *foreground\n      bgColor: *comment\n      activeColor: *blue\n    status:\n      newColor: *cyan\n      modifyColor: *blue\n      addColor: *green\n      errorColor: *red\n      highlightColor: *orange\n      killColor: *comment\n      completedColor: *comment\n    title:\n      fgColor: *foreground\n      bgColor: *background\n      highlightColor: *orange\n      counterColor: *blue\n      filterColor: *magenta\n  views:\n    charts:\n      bgColor: background\n      defaultDialColors:\n        - *blue\n        - *red\n      defaultChartColors:\n        - *blue\n        - *red\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      cursorFgColor: *foreground\n      cursorBgColor: *current_line\n      header:\n        fgColor: *foreground\n        bgColor: *background\n        sorterColor: *selection\n    xray:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *current_line\n      graphicColor: *blue\n      showIcons: false\n    yaml:\n      keyColor: *magenta\n      colonColor: *blue\n      valueColor: *foreground\n    logs:\n      fgColor: *foreground\n      bgColor: *background\n      indicator:\n        fgColor: *foreground\n        bgColor: *background\n        toggleOnColor: *magenta\n        toggleOffColor: *blue\n"
  },
  {
    "path": "skins/gruvbox-material-dark-hard.yaml",
    "content": "# ----------------------------------------\n# Gruvbox Material Dark Hard Theme for k9s\n# ----------------------------------------\nforeground: &foreground \"#d4be98\"\nbackground: &background \"#1d2021\"\ncurrent_line: &current_line \"#d4be98\"\nselection: &selection \"#3c3836\"\ncomment: &comment \"#928374\"\ncyan: &cyan \"#7daea3\"\ngreen: &green \"#a9b665\"\norange: &orange \"#e78a4e\"\nmagenta: &magenta \"#d3869b\"\nblue: &blue \"#7daea3\"\nred: &red \"#ea6962\"\n\nk9s:\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *blue\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *orange\n  info:\n    fgColor: *magenta\n    sectionColor: *foreground\n  help:\n    fgColor: *foreground\n    bgColor: *background\n    keyColor: *magenta\n    numKeyColor: *blue\n    sectionColor: *green\n  dialog:\n    fgColor: *foreground\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *magenta\n    buttonFocusFgColor: white\n    buttonFocusBgColor: *cyan\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  frame:\n    border:\n      fgColor: *selection\n      focusColor: *current_line\n    menu:\n      fgColor: *foreground\n      keyColor: *magenta\n      numKeyColor: *magenta\n    crumbs:\n      fgColor: *foreground\n      bgColor: *comment\n      activeColor: *blue\n    status:\n      newColor: *cyan\n      modifyColor: *blue\n      addColor: *green\n      errorColor: *red\n      highlightColor: *orange\n      killColor: *comment\n      completedColor: *comment\n    title:\n      fgColor: *foreground\n      bgColor: *background\n      highlightColor: *orange\n      counterColor: *blue\n      filterColor: *magenta\n  views:\n    charts:\n      bgColor: *background\n      defaultDialColors:\n        - *blue\n        - *red\n      defaultChartColors:\n        - *blue\n        - *red\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      cursorFgColor: \"#ffffff\"\n      cursorBgColor: *current_line\n      header:\n        fgColor: *foreground\n        bgColor: *background\n        sorterColor: *selection\n    xray:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *current_line\n      graphicColor: *blue\n      showIcons: false\n    yaml:\n      keyColor: *magenta\n      colonColor: *blue\n      valueColor: *foreground\n    logs:\n      fgColor: *foreground\n      bgColor: *background\n      indicator:\n        fgColor: *foreground\n        bgColor: *background\n\n"
  },
  {
    "path": "skins/gruvbox-material-dark-medium.yaml",
    "content": "# ------------------------------------------\n# Gruvbox Material Dark Medium Theme for k9s\n# ------------------------------------------\nforeground: &foreground \"#d4be98\"\nbackground: &background \"#282828\"\ncurrent_line: &current_line \"#d4be98\"\nselection: &selection \"#3c3836\"\ncomment: &comment \"#928374\"\ncyan: &cyan \"#89b482\"\ngreen: &green \"#a9b665\"\norange: &orange \"#e78a4e\"\nmagenta: &magenta \"#d3869b\"\nblue: &blue \"#7daea3\"\nred: &red \"#ea6962\"\n\nk9s:\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *blue\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *orange\n  info:\n    fgColor: *magenta\n    sectionColor: *foreground\n  help:\n    fgColor: *foreground\n    bgColor: *background\n    keyColor: *magenta\n    numKeyColor: *blue\n    sectionColor: *green\n  dialog:\n    fgColor: *foreground\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *magenta\n    buttonFocusFgColor: white\n    buttonFocusBgColor: *cyan\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  frame:\n    border:\n      fgColor: *selection\n      focusColor: *current_line\n    menu:\n      fgColor: *foreground\n      keyColor: *magenta\n      numKeyColor: *magenta\n    crumbs:\n      fgColor: *foreground\n      bgColor: *comment\n      activeColor: *blue\n    status:\n      newColor: *cyan\n      modifyColor: *blue\n      addColor: *green\n      errorColor: *red\n      highlightColor: *orange\n      killColor: *comment\n      completedColor: *comment\n    title:\n      fgColor: *foreground\n      bgColor: *background\n      highlightColor: *orange\n      counterColor: *blue\n      filterColor: *magenta\n  views:\n    charts:\n      bgColor: *background\n      defaultDialColors:\n        - *blue\n        - *red\n      defaultChartColors:\n        - *blue\n        - *red\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      cursorFgColor: \"#ffffff\"\n      cursorBgColor: *current_line\n      header:\n        fgColor: *foreground\n        bgColor: *background\n        sorterColor: *selection\n    xray:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *current_line\n      graphicColor: *blue\n      showIcons: false\n    yaml:\n      keyColor: *magenta\n      colonColor: *blue\n      valueColor: *foreground\n    logs:\n      fgColor: *foreground\n      bgColor: *background\n      indicator:\n        fgColor: *foreground\n        bgColor: *background\n\n"
  },
  {
    "path": "skins/gruvbox-material-dark-soft.yaml",
    "content": "# ----------------------------------------\n# Gruvbox Material Dark Soft Theme for k9s\n# ----------------------------------------\nforeground: &foreground \"#ddc7a1\"\nbackground: &background \"#32302f\"\ncurrent_line: &current_line \"#ddc7a1\"\nselection: &selection \"#3c3836\"\ncomment: &comment \"#928374\"\ncyan: &cyan \"#89b482\"\ngreen: &green \"#a9b665\"\norange: &orange \"#e78a4e\"\nmagenta: &magenta \"#d3869b\"\nblue: &blue \"#7daea3\"\nred: &red \"#ea6962\"\n\nk9s:\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *blue\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *orange\n  info:\n    fgColor: *magenta\n    sectionColor: *foreground\n  help:\n    fgColor: *foreground\n    bgColor: *background\n    keyColor: *magenta\n    numKeyColor: *blue\n    sectionColor: *green\n  dialog:\n    fgColor: *foreground\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *magenta\n    buttonFocusFgColor: white\n    buttonFocusBgColor: *cyan\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  frame:\n    border:\n      fgColor: *selection\n      focusColor: *current_line\n    menu:\n      fgColor: *foreground\n      keyColor: *magenta\n      numKeyColor: *magenta\n    crumbs:\n      fgColor: *foreground\n      bgColor: *comment\n      activeColor: *blue\n    status:\n      newColor: *cyan\n      modifyColor: *blue\n      addColor: *green\n      errorColor: *red\n      highlightColor: *orange\n      killColor: *comment\n      completedColor: *comment\n    title:\n      fgColor: *foreground\n      bgColor: *background\n      highlightColor: *orange\n      counterColor: *blue\n      filterColor: *magenta\n  views:\n    charts:\n      bgColor: *background\n      defaultDialColors:\n        - *blue\n        - *red\n      defaultChartColors:\n        - *blue\n        - *red\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      cursorFgColor: \"#ffffff\"\n      cursorBgColor: *current_line\n      header:\n        fgColor: *foreground\n        bgColor: *background\n        sorterColor: *selection\n    xray:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *current_line\n      graphicColor: *blue\n      showIcons: false\n    yaml:\n      keyColor: *magenta\n      colonColor: *blue\n      valueColor: *foreground\n    logs:\n      fgColor: *foreground\n      bgColor: *background\n      indicator:\n        fgColor: *foreground\n        bgColor: *background\n\n"
  },
  {
    "path": "skins/gruvbox-material-light-hard.yaml",
    "content": "# -----------------------------------------\n# Gruvbox Material Light Hard Theme for k9s\n# -----------------------------------------\nforeground: &foreground \"#654735\"\nbackground: &background \"#f9f5d7\"\ncurrent_line: &current_line \"#654735\"\nselection: &selection \"#d5c4a1\"\ncomment: &comment \"#9d0006\"\ncyan: &cyan \"#689d6a\"\ngreen: &green \"#98971a\"\norange: &orange \"#d79921\"\nmagenta: &magenta \"#b16286\"\nblue: &blue \"#458588\"\nred: &red \"#cc241d\"\n\nk9s:\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *blue\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *orange\n  info:\n    fgColor: *magenta\n    sectionColor: *foreground\n  help:\n    fgColor: *foreground\n    bgColor: *background\n    keyColor: *magenta\n    numKeyColor: *blue\n    sectionColor: *green\n  dialog:\n    fgColor: *foreground\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *magenta\n    buttonFocusFgColor: white\n    buttonFocusBgColor: *cyan\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  frame:\n    border:\n      fgColor: *selection\n      focusColor: *current_line\n    menu:\n      fgColor: *foreground\n      keyColor: *magenta\n      numKeyColor: *magenta\n    crumbs:\n      fgColor: *foreground\n      bgColor: *comment\n      activeColor: *blue\n    status:\n      newColor: *cyan\n      modifyColor: *blue\n      addColor: *green\n      errorColor: *red\n      highlightColor: *orange\n      killColor: *comment\n      completedColor: *comment\n    title:\n      fgColor: *foreground\n      bgColor: *background\n      highlightColor: *orange\n      counterColor: *blue\n      filterColor: *magenta\n  views:\n    charts:\n      bgColor: *background\n      defaultDialColors: [*blue, *red]\n      defaultChartColors: [*blue, *red]\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      cursorFgColor: \"#ffffff\"\n      cursorBgColor: *current_line\n      header:\n        fgColor: *foreground\n        bgColor: *background\n        sorterColor: *selection\n    xray:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *current_line\n      graphicColor: *blue\n      showIcons: false\n    yaml:\n      keyColor: *magenta\n      colonColor: *blue\n      valueColor: *foreground\n    logs:\n      fgColor: *foreground\n      bgColor: *background\n      indicator:\n        fgColor: *foreground\n        bgColor: *background\n\n"
  },
  {
    "path": "skins/gruvbox-material-light-medium.yaml",
    "content": "# -------------------------------------------\n# Gruvbox Material Light Medium Theme for k9s\n# -------------------------------------------\nforeground: &foreground \"#654735\"\nbackground: &background \"#fbf1c7\"\ncurrent_line: &current_line \"#654735\"\nselection: &selection \"#d5c4a1\"\ncomment: &comment \"#9d0006\"\ncyan: &cyan \"#689d6a\"\ngreen: &green \"#98971a\"\norange: &orange \"#d79921\"\nmagenta: &magenta \"#b16286\"\nblue: &blue \"#458588\"\nred: &red \"#cc241d\"\n\nk9s:\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *blue\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *orange\n  info:\n    fgColor: *magenta\n    sectionColor: *foreground\n  help:\n    fgColor: *foreground\n    bgColor: *background\n    keyColor: *magenta\n    numKeyColor: *blue\n    sectionColor: *green\n  dialog:\n    fgColor: *foreground\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *magenta\n    buttonFocusFgColor: white\n    buttonFocusBgColor: *cyan\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  frame:\n    border:\n      fgColor: *selection\n      focusColor: *current_line\n    menu:\n      fgColor: *foreground\n      keyColor: *magenta\n      numKeyColor: *magenta\n    crumbs:\n      fgColor: *foreground\n      bgColor: *comment\n      activeColor: *blue\n    status:\n      newColor: *cyan\n      modifyColor: *blue\n      addColor: *green\n      errorColor: *red\n      highlightColor: *orange\n      killColor: *comment\n      completedColor: *comment\n    title:\n      fgColor: *foreground\n      bgColor: *background\n      highlightColor: *orange\n      counterColor: *blue\n      filterColor: *magenta\n  views:\n    charts:\n      bgColor: *background\n      defaultDialColors: [*blue, *red]\n      defaultChartColors: [*blue, *red]\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      cursorFgColor: \"#ffffff\"\n      cursorBgColor: *current_line\n      header:\n        fgColor: *foreground\n        bgColor: *background\n        sorterColor: *selection\n    xray:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *current_line\n      graphicColor: *blue\n      showIcons: false\n    yaml:\n      keyColor: *magenta\n      colonColor: *blue\n      valueColor: *foreground\n    logs:\n      fgColor: *foreground\n      bgColor: *background\n      indicator:\n        fgColor: *foreground\n        bgColor: *background\n\n"
  },
  {
    "path": "skins/gruvbox-material-light-soft.yaml",
    "content": "# ----------------------------------------\n# Grvbox Material Light Soft Theme for k9s\n# ----------------------------------------\nforeground: &foreground \"#654735\"\nbackground: &background \"#f2e5bc\"\ncurrent_line: &current_line \"#654735\"\nselection: &selection \"#d5c4a1\"\ncomment: &comment \"#9d0006\"\ncyan: &cyan \"#689d6a\"\ngreen: &green \"#98971a\"\norange: &orange \"#d79921\"\nmagenta: &magenta \"#b16286\"\nblue: &blue \"#458588\"\nred: &red \"#cc241d\"\n\nk9s:\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *blue\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *orange\n  info:\n    fgColor: *magenta\n    sectionColor: *foreground\n  help:\n    fgColor: *foreground\n    bgColor: *background\n    keyColor: *magenta\n    numKeyColor: *blue\n    sectionColor: *green\n  dialog:\n    fgColor: *foreground\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *magenta\n    buttonFocusFgColor: white\n    buttonFocusBgColor: *cyan\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  frame:\n    border:\n      fgColor: *selection\n      focusColor: *current_line\n    menu:\n      fgColor: *foreground\n      keyColor: *magenta\n      numKeyColor: *magenta\n    crumbs:\n      fgColor: *foreground\n      bgColor: *comment\n      activeColor: *blue\n    status:\n      newColor: *cyan\n      modifyColor: *blue\n      addColor: *green\n      errorColor: *red\n      highlightColor: *orange\n      killColor: *comment\n      completedColor: *comment\n    title:\n      fgColor: *foreground\n      bgColor: *background\n      highlightColor: *orange\n      counterColor: *blue\n      filterColor: *magenta\n  views:\n    charts:\n      bgColor: *background\n      defaultDialColors: [*blue, *red]\n      defaultChartColors: [*blue, *red]\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      cursorFgColor: \"#ffffff\"\n      cursorBgColor: *current_line\n      header:\n        fgColor: *foreground\n        bgColor: *background\n        sorterColor: *selection\n    xray:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *current_line\n      graphicColor: *blue\n      showIcons: false\n    yaml:\n      keyColor: *magenta\n      colonColor: *blue\n      valueColor: *foreground\n    logs:\n      fgColor: *foreground\n      bgColor: *background\n      indicator:\n        fgColor: *foreground\n        bgColor: *background\n\n"
  },
  {
    "path": "skins/in-the-navy.yaml",
    "content": "# -----------------------------------------------------------------------------\n# In the Navy\n# -----------------------------------------------------------------------------\n\n# Styles...\nfg: &fg \"dodgerblue\"\nbg: &bg \"white\"\nblue: &blue \"blue\"\nsky: &sky \"lightskyblue\"\nsteel: &steel \"steelblue\"\ndark: &dark \"darkblue\"\nalice: &alice \"aliceblue\"\ncorn: &corn \"cornflowerblue\"\nerr: &err \"indianred\"\nroyal: &royal \"royalblue\"\nslate: &slate \"slategray\"\ngray: &gray \"gray\"\ncadet: &cadet \"cadetblue\"\npowder: &powder \"powderblue\"\naqua: &aqua \"aqua\"\nmslate: &mslate \"mediumslateblue\"\n\n# Skin...\nk9s:\n  body:\n    fgColor: *fg\n    bgColor: *bg\n    logoColor: *blue\n  prompt:\n    fgColor: *fg\n    bgColor: *bg\n    suggestColor: *cadet\n  info:\n    fgColor: *sky\n    sectionColor: *steel\n  dialog:\n    fgColor: *fg\n    bgColor: *bg\n    buttonFgColor: *fg\n    buttonBgColor: *powder\n    buttonFocusFgColor: white\n    buttonFocusBgColor: *aqua\n    labelFgColor: *mslate\n    fieldFgColor: *fg\n  frame:\n    border:\n      fgColor: *fg\n      bgColor: *dark\n      focusColor: *alice\n    menu:\n      fgColor: *dark\n      keyColor: *corn\n      numKeyColor: *cadet\n    crumbs:\n      fgColor: *bg\n      bgColor: *steel\n      activeColor: *sky\n    status:\n      newColor: *blue\n      modifyColor: *powder\n      addColor: *sky\n      errorColor: *err\n      highlightColor: *royal\n      killColor: *slate\n      completedColor: *gray\n    title:\n      fgColor: *cadet\n      bgColor: *bg\n      highlightColor: *sky\n      counterColor: *slate\n      filterColor: *slate\n  views:\n    table:\n      fgColor: *fg\n      bgColor: *bg\n      cursorFgColor: *fg\n      cursorBgColor: *aqua\n      markColor: *mslate\n      header:\n        fgColor: *fg\n        bgColor: *bg\n        sorterColor: *cadet\n    xray:\n      fgColor: *blue\n      bgColor: *dark\n      cursorColor: *aqua\n      graphicColor: *mslate\n      showIcons: false\n    charts:\n      bgColor: *bg\n      defaultDialColors:\n        - *aqua\n        - *err\n      defaultChartColors:\n        - *aqua\n        - *err\n    yaml:\n      keyColor: *steel\n      colorColor: *blue\n      valueColor: *royal\n    logs:\n      fgColor: *dark\n      bgColor: *bg\n      indicator:\n        fgColor: *dark\n        bgColor: *bg\n        toggleOnColor: *steel\n        toggleOffColor: *blue\n"
  },
  {
    "path": "skins/kanagawa.yaml",
    "content": "# -----------------------------------------------------------------------------\n# Kanagawa Skin\n# -----------------------------------------------------------------------------\n\n# Styles...\nforeground: &foreground \"#dcd7ba\"\nbackground: &background \"#1f1f28\"\nblack: &black \"#090618\"\nblue: &blue \"#7e9cd8\"\ngreen: &green \"#76946a\"\ngrey: &grey \"#727169\"\norange: &orange \"#ffa066\"\npurple: &purple \"#957fb8\"\nred: &red \"#c34043\"\nyellow: &yellow \"#c0a36e\"\nyellow_bright: &yellow_bright \"#e6c384\"\n\n# Skin...\nk9s:\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *green\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *orange\n  info:\n    fgColor: *grey\n    sectionColor: *green\n  help:\n    fgColor: *foreground\n    bgColor: *background\n    keyColor: *yellow\n    numKeyColor: *blue\n    sectionColor: *purple\n  dialog:\n    fgColor: *black\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *green\n    buttonFocusFgColor: *black\n    buttonFocusBgColor: *blue\n    labelFgColor: *orange\n    fieldFgColor: *blue\n  frame:\n    border:\n      fgColor: *green\n      focusColor: *green\n    menu:\n      fgColor: *grey\n      keyColor: *yellow\n      numKeyColor: *yellow\n    crumbs:\n      fgColor: *black\n      bgColor: *green\n      activeColor: *yellow\n    status:\n      newColor: *blue\n      modifyColor: *green\n      addColor: *grey\n      pendingColor: *orange\n      errorColor: *red\n      highlightColor: *yellow\n      killColor: *purple\n      completedColor: *grey\n    title:\n      fgColor: *blue\n      bgColor: *background\n      highlightColor: *purple\n      counterColor: *foreground\n      filterColor: *blue\n  views:\n    charts:\n      bgColor: *background\n      defaultDialColors:\n        - *green\n        - *red\n      defaultChartColors:\n        - *green\n        - *red\n    table:\n      fgColor: *yellow\n      bgColor: *background\n      cursorFgColor: *black\n      cursorBgColor: *blue\n      markColor: *yellow_bright\n      header:\n        fgColor: *grey\n        bgColor: *background\n        sorterColor: *orange\n    xray:\n      fgColor: *blue\n      bgColor: *background\n      cursorColor: *foreground\n      graphicColor: *yellow_bright\n      showIcons: false\n    yaml:\n      keyColor: *red\n      colonColor: *grey\n      valueColor: *grey\n    logs:\n      fgColor: *grey\n      bgColor: *background\n      indicator:\n        fgColor: *blue\n        bgColor: *background\n        toggleOnColor: *red\n        toggleOffColor: *grey\n    help:\n      fgColor: *grey\n      bgColor: *background\n      indicator:\n        fgColor: *blue\n"
  },
  {
    "path": "skins/kiss.yaml",
    "content": "# -----------------------------------------------------------------------------\n# K9s Kiss Skin\n# Author: [@beejeebus](justin.p.randell@gmail.com)\n# -----------------------------------------------------------------------------\n\n# Skin...\nk9s:\n  body:\n    fgColor: default\n    bgColor: default\n    logoColor: default\n  prompt:\n    fgColor: default\n    bgColor: default\n    suggestColor: default\n  info:\n    fgColor: default\n    sectionColor: default\n  dialog:\n    fgColor: default\n    bgColor: default\n    buttonFgColor: default\n    buttonBgColor: default\n    buttonFocusFgColor: default\n    buttonFocusBgColor: default\n    labelFgColor: default\n    fieldFgColor: default\n  frame:\n    border:\n      fgColor: default\n      focusColor: default\n    menu:\n      fgColor: default\n      keyColor: default\n      numKeyColor: default\n    crumbs:\n      fgColor: default\n      bgColor: default\n      activeColor: default\n    status:\n      newColor: default\n      modifyColor: default\n      addColor: default\n      errorColor: default\n      highlightColor: default\n      killColor: default\n      completedColor: default\n    title:\n      fgColor: default\n      bgColor: default\n      highlightColor: default\n      counterColor: default\n      filterColor: default\n  views:\n    table:\n      fgColor: default\n      bgColor: default\n      cursorFgColor: default\n      cursorBfColor: default\n      header:\n        fgColor: default\n        bgColor: default\n        sorterColor: default\n    yaml:\n      keyColor: default\n      colonColor: default\n      valueColor: default\n    logs:\n      fgColor: default\n      bgColor: default\n      indicator:\n        fgColor: default\n        bgColor: default\n        toggleOnColor: default\n        toggleOffColor: default\n"
  },
  {
    "path": "skins/monokai.yaml",
    "content": "# -----------------------------------------------------------------------------\n# Monokai skin\n# -----------------------------------------------------------------------------\n\n# Styles...\nforeground: &foreground \"#ffffff\"\nbackground: &background \"default\"\nbackgroundOpaque: &backgroundOpaque \"#333333\"\nmagenta: &magenta \"#f72972\"\norange: &orange \"#e47c20\"\nlightBlue: &lightBlue \"#c3eff7\"\nblue: &blue \"#69d9ed\"\ndarkBlue: &darkBlue \"#3174a2\"\ngreen: &green \"#a7e24c\"\npurple: &purple \"#856cc4\"\nyellow: &yellow \"#e1df8f\"\ndarkGray: &darkGray \"#666666\"\n\n# Skin...\nk9s:\n  # General K9s styles\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *purple\n    logoColorMsg: *foreground\n    logoColorInfo: *lightBlue\n    logoColorWarn: *orange\n    logoColorError: *magenta\n\n  # Command prompt styles\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *darkGray\n\n  # ClusterInfoView styles.\n  info:\n    fgColor: *magenta\n    sectionColor: *yellow\n\n  # Help Menu styles\n  help:\n    fgColor: *foreground\n    bgColor: *background\n    keyColor: *green\n    numKeyColor: *green\n    sectionColor: *blue\n\n  # Dialog styles.\n  dialog:\n    fgColor: *yellow\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *background\n    buttonFocusFgColor: *foreground\n    buttonFocusBgColor: *purple\n    labelFgColor: *magenta\n    fieldFgColor: *darkBlue\n\n  frame:\n    # Borders styles.\n    border:\n      fgColor: *darkGray\n      focusColor: *darkGray\n\n    menu:\n      fgColor: *foreground\n      keyColor: *magenta\n      # Used for favorite namespaces\n      numKeyColor: *green\n\n    # CrumbView attributes for history navigation.\n    crumbs:\n      fgColor: *yellow\n      bgColor: *backgroundOpaque\n      activeColor: *purple\n\n    # Resource status and update styles\n    status:\n      newColor: *blue\n      modifyColor: *purple\n      addColor: *green\n      pendingColor: *orange\n      errorColor: *magenta\n      highlightColor: *blue\n      killColor: *magenta\n      completedColor: *darkBlue\n\n    # Border title styles.\n    title:\n      fgColor: *purple\n      bgColor: *background\n      highlightColor: *yellow\n      counterColor: *green\n      filterColor: *orange\n\n  # Specific views styles\n  views:\n    # Charts skins...\n    charts:\n      bgColor: *background\n      dialBgColor: *background\n      chartBgColor: *backgroundOpaque\n      defaultDialColors:\n        - *blue\n        - *magenta\n      defaultChartColors:\n        - *blue\n        - *magenta\n      resourceColors:\n        batch/v1/jobs:\n          - *blue\n          - *magenta\n        v1/persistentvolumes:\n          - *blue\n          - *magenta\n        cpu:\n          - *blue\n          - *magenta\n        mem:\n          - *blue\n          - *magenta\n        v1/events:\n          - *blue\n          - *magenta\n        v1/pods:\n          - *blue\n          - *magenta\n\n    # TableView attributes.\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      cursorFgColor: *foreground\n      cursorBgColor: *backgroundOpaque\n      markColor: *magenta\n      # Header row styles.\n      header:\n        fgColor: *foreground\n        bgColor: *backgroundOpaque\n        sorterColor: *magenta\n\n    # Xray view attributes.\n    xray:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *blue\n      cursorTextColor: *foreground\n      graphicColor: *blue\n\n    # YAML info styles.\n    yaml:\n      keyColor: *green\n      colonColor: *magenta\n      valueColor: *foreground\n\n    # Logs styles.\n    logs:\n      fgColor: *foreground\n      bgColor: *background\n      indicator:\n        fgColor: *foreground\n        bgColor: *backgroundOpaque\n        toggleOnColor: *green\n        toggleOffColor: *magenta\n"
  },
  {
    "path": "skins/narsingh.yaml",
    "content": "# -----------------------------------------------------------------------------\n# Narsingh skin\n# -----------------------------------------------------------------------------\n\n# Skin...\nk9s:\n  # General K9s styles\n  body:\n    fgColor: \"#97979b\"\n    bgColor: \"#282a36\"\n    logoColor: \"#5af78e\"\n  prompt:\n    fgColor: \"#97979b\"\n    bgColor: \"#282a36\"\n    suggestColor: \"#5af78e\"\n  info:\n    fgColor: white\n    sectionColor: \"#5af78e\"\n  dialog:\n    fgColor: \"#97979b\"\n    bgColor: \"#282a36\"\n    buttonFgColor: \"#97979b\"\n    buttonBgColor: \"#282a36\"\n    buttonFocusFgColor: \"#97979b\"\n    buttonFocusBgColor: \"#5af78e\"\n    labelFgColor: \"#97979b\"\n    fieldFgColor: \"#5af78e\"\n  frame:\n    border:\n      fgColor: \"#5af78e\"\n      focusColor: \"#5af78e\"\n    menu:\n      fgColor: white\n      keyColor: \"#57c7ff\"\n      numKeyColor: \"#ff6ac1\"\n    crumbs:\n      fgColor: \"#282a36\"\n      bgColor: white\n      activeColor: \"#f3f99d\"\n    status:\n      newColor: \"#eff0eb\"\n      modifyColor: \"#5af78e\"\n      addColor: \"#57c7ff\"\n      errorColor: \"#ff5c57\"\n      highlightColor: \"#f3f99d\"\n      killColor: mediumpurple\n      completedColor: gray\n    title:\n      fgColor: \"#5af78e\"\n      bgColor: \"#282a36\"\n      highlightColor: white\n      counterColor: white\n      filterColor: \"#57c7ff\"\n  views:\n    # Charts skins...\n    charts:\n      bgColor: default\n      defaultDialColors:\n        - \"#57c7ff\"\n        - \"#ff5c57\"\n      defaultChartColors:\n        - \"#57c7ff\"\n        - \"#ff5c57\"\n    table:\n      fgColor: \"#57c7ff\"\n      bgColor: \"#282a36\"\n      markColor: darkgoldenrod\n      header:\n        fgColor: white\n        bgColor: \"#282a36\"\n        sorterColor: orange\n    xray:\n      fgColor: \"#57c7ff\"\n      bgColor: \"#282a36\"\n      cursorColor: \"#5af78e\"\n      graphicColor: darkgoldenrod\n      showIcons: false\n    yaml:\n      keyColor: \"#ff5c57\"\n      colonColor: white\n      valueColor: \"#f3f99d\"\n    logs:\n      fgColor: white\n      bgColor: \"#282a36\"\n      indicator:\n        fgColor: white\n        bgColor: \"#282a36\"\n        toggleOnColor: \"#ff5c57\"\n        toggleOffColor: \"#f3f99d\"\n"
  },
  {
    "path": "skins/nightfox.yaml",
    "content": "# -----------------------------------------------------------------------------\n# K9s Nightfox Theme\n# Based on the Nightfox.nvim color scheme:\n# https://github.com/EdenEast/nightfox.nvim\n# -----------------------------------------------------------------------------\n\n# Styles...\nforeground: &foreground \"#cdcecf\"\nbackground: &background \"#192330\"\ncurrent_line: &current_line \"#2b3b51\"\nselection: &selection \"#2b3b51\"\ncomment: &comment \"#738091\"\ncyan: &cyan \"#63cdcf\"\ngreen: &green \"#81b29a\"\norange: &orange \"#f4a261\"\nmagenta: &magenta \"#9d79d6\"\nblue: &blue \"#719cd6\"\nred: &red \"#c94f6d\"\n\n# Skin...\nk9s:\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *blue\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *orange\n  info:\n    fgColor: *magenta\n    sectionColor: *foreground\n  dialog:\n    fgColor: *foreground\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *magenta\n    buttonFocusFgColor: white\n    buttonFocusBgColor: *cyan\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  frame:\n    border:\n      fgColor: *selection\n      focusColor: *current_line\n    menu:\n      fgColor: *foreground\n      keyColor: *magenta\n      numKeyColor: *magenta\n    crumbs:\n      fgColor: *foreground\n      bgColor: *current_line\n      activeColor: *current_line\n    status:\n      newColor: *cyan\n      modifyColor: *blue\n      addColor: *green\n      errorColor: *red\n      highlightColor: *orange\n      killColor: *comment\n      completedColor: *comment\n    title:\n      fgColor: *foreground\n      bgColor: *current_line\n      highlightColor: *orange\n      counterColor: *blue\n      filterColor: *magenta\n  views:\n    charts:\n      bgColor: default\n      defaultDialColors:\n        - *blue\n        - *red\n      defaultChartColors:\n        - *blue\n        - *red\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      cursorFgColor: *selection\n      cursorBgColor: *current_line\n      header:\n        fgColor: *foreground\n        bgColor: *background\n        sorterColor: *cyan\n    xray:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *current_line\n      graphicColor: *blue\n      showIcons: false\n    yaml:\n      keyColor: *magenta\n      colonColor: *blue\n      valueColor: *foreground\n    logs:\n      fgColor: *foreground\n      bgColor: *background\n      indicator:\n        fgColor: *foreground\n        bgColor: *selection\n        toggleOnColor: *magenta\n        toggleOffColor: *blue\n"
  },
  {
    "path": "skins/nord.yaml",
    "content": "# -----------------------------------------------------------------------------\n# Nord skin\n# -----------------------------------------------------------------------------\n\n# Styles...\nforeground: &foreground \"#DADEE8\"\nbackground: &background \"#30343F\"\ncurrent_line: &current_line \"#383D4A\"\nselection: &selection \"#D9DEE8\"\ncomment: &comment \"#8891A7\"\ncyan: &cyan \"#88C0D0\"\ngreen: &green \"#A3BE8C\"\norange: &orange \"#D08770\"\nblue: &blue \"#81A1C1\"\nmagenta: &magenta \"#B48EAD\"\nred: &red \"#BF616A\"\nyellow: &yellow \"#EBCB8B\"\n\n# Skin...\nk9s:\n  # General K9s styles\n  body:\n    fgColor: *foreground\n    bgColor: default\n    logoColor: *magenta\n  # Command prompt styles\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *orange\n  # ClusterInfoView styles.\n  info:\n    fgColor: *blue\n    sectionColor: *foreground\n  # Dialog styles.\n  dialog:\n    fgColor: *foreground\n    bgColor: default\n    buttonFgColor: *foreground\n    buttonBgColor: *magenta\n    buttonFocusFgColor: *yellow\n    buttonFocusBgColor: *blue\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  frame:\n    # Borders styles.\n    border:\n      fgColor: *selection\n      focusColor: *current_line\n    menu:\n      fgColor: *foreground\n      keyColor: *blue\n      # Used for favorite namespaces\n      numKeyColor: *blue\n    # CrumbView attributes for history navigation.\n    crumbs:\n      fgColor: *foreground\n      bgColor: *current_line\n      activeColor: *current_line\n    # Resource status and update styles\n    status:\n      newColor: *cyan\n      modifyColor: *magenta\n      addColor: *green\n      errorColor: *red\n      highlightColor: *orange\n      killColor: *comment\n      completedColor: *comment\n    # Border title styles.\n    title:\n      fgColor: *foreground\n      bgColor: *current_line\n      highlightColor: *orange\n      counterColor: *magenta\n      filterColor: *blue\n  views:\n    # Charts skins...\n    charts:\n      bgColor: default\n      defaultDialColors:\n        - *magenta\n        - *red\n      defaultChartColors:\n        - *magenta\n        - *red\n    # TableView attributes.\n    table:\n      fgColor: *foreground\n      bgColor: default\n      # Header row styles.\n      header:\n        fgColor: *foreground\n        bgColor: default\n        sorterColor: *cyan\n    # Xray view attributes.\n    xray:\n      fgColor: *foreground\n      bgColor: default\n      cursorColor: *current_line\n      graphicColor: *magenta\n      showIcons: false\n    # YAML info styles.\n    yaml:\n      keyColor: *blue\n      colonColor: *magenta\n      valueColor: *foreground\n    # Logs styles.\n    logs:\n      fgColor: *foreground\n      bgColor: default\n      indicator:\n        fgColor: *foreground\n        bgColor: *magenta\n        toggleOnColor: *magenta\n        toggleOffColor: *blue\n    help:\n      fgColor: *foreground\n      bgColor: *background\n      indicator:\n        fgColor: *red\n"
  },
  {
    "path": "skins/one-dark.yaml",
    "content": "# -----------------------------------------------------------------------------\n# OneDark Skin\n# -----------------------------------------------------------------------------\n\n# Styles...\nforeground: &foreground \"#abb2bf\"\nbackground: &background \"#282c34\"\nblack: &black \"#080808\"\nblue: &blue \"#61afef\"\ngreen: &green \"#98c379\"\ngrey: &grey \"#abb2bf\"\norange: &orange \"#ffb86c\"\npurple: &purple \"#c678dd\"\nred: &red \"#e06370\"\nyellow: &yellow \"#e5c07b\"\nyellow_bright: &yellow_bright \"#d19a66\"\n\n# Skin...\nk9s:\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *green\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *orange\n  info:\n    fgColor: *grey\n    sectionColor: *green\n  help:\n    fgColor: *foreground\n    bgColor: *background\n    keyColor: *yellow\n    numKeyColor: *blue\n    sectionColor: *purple\n  dialog:\n    fgColor: *black\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *green\n    buttonFocusFgColor: *black\n    buttonFocusBgColor: *blue\n    labelFgColor: *orange\n    fieldFgColor: *blue\n  frame:\n    border:\n      fgColor: *green\n      focusColor: *green\n    menu:\n      fgColor: *grey\n      keyColor: *yellow\n      numKeyColor: *yellow\n    crumbs:\n      fgColor: *black\n      bgColor: *green\n      activeColor: *yellow\n    status:\n      newColor: *blue\n      modifyColor: *green\n      addColor: *grey\n      pendingColor: *orange\n      errorColor: *red\n      highlightColor: *yellow\n      killColor: *purple\n      completedColor: *grey\n    title:\n      fgColor: *blue\n      bgColor: *background\n      highlightColor: *purple\n      counterColor: *foreground\n      filterColor: *blue\n  views:\n    charts:\n      bgColor: *background\n      defaultDialColors:\n        - *green\n        - *red\n      defaultChartColors:\n        - *green\n        - *red\n    table:\n      fgColor: *yellow\n      bgColor: *background\n      cursorFgColor: *black\n      cursorBgColor: *blue\n      markColor: *yellow_bright\n      header:\n        fgColor: *grey\n        bgColor: *background\n        sorterColor: *orange\n    xray:\n      fgColor: *blue\n      bgColor: *background\n      cursorColor: *foreground\n      graphicColor: *yellow_bright\n      showIcons: false\n    yaml:\n      keyColor: *red\n      colonColor: *grey\n      valueColor: *grey\n    logs:\n      fgColor: *grey\n      bgColor: *background\n      indicator:\n        fgColor: *blue\n        bgColor: *background\n        toggleOnColor: *red\n        toggleOffColor: *grey\n    help:\n      fgColor: *grey\n      bgColor: *background\n      indicator:\n        fgColor: *blue\n"
  },
  {
    "path": "skins/red.yaml",
    "content": "# -----------------------------------------------------------------------------\n# Red skin\n# -----------------------------------------------------------------------------\n\n# Skin...\nk9s:\n  body:\n    fgColor: red\n    bgColor: black\n    logoColor: red\n  prompt:\n    fgColor: red\n    bgColor: black\n    suggestColor: red\n  info:\n    fgColor: red\n    sectionColor: red\n  dialog:\n    fgColor: red\n    bgColor: black\n    buttonFgColor: black\n    buttonBgColor: red\n    buttonFocusFgColor: white\n    buttonFocusBgColor: red\n    labelFgColor: red\n    fieldFgColor: red\n  frame:\n    border:\n      fgColor: red\n      focusColor: red\n    menu:\n      fgColor: white\n      keyColor: red\n      numKeyColor: red\n    crumbs:\n      fgColor: black\n      bgColor: red\n      activeColor: red\n    status:\n      newColor: red\n      modifyColor: greenyellow\n      addColor: white\n      errorColor: red\n      pendingColor: darkred\n      highlightColor: red\n      killColor: red\n      completedColor: gray\n    title:\n      fgColor: red\n      highlightColor: red\n      counterColor: red\n      filterColor: red\n  views:\n    charts:\n      bgColor: black\n      defaultDialColors:\n        - linegreen\n        - redred\n      defaultChartColors:\n        - linegreen\n        - redred\n    table:\n      fgColor: red\n      bgColor: black\n      cursorFgColor: black\n      cursorBgColor: red\n      markColor: darkgoldenrod\n      header:\n        fgColor: red\n        bgColor: black\n        sorterColor: red\n    xray:\n      fgColor: red\n      bgColor: black\n      cursorColor: red\n      graphicColor: darkgoldenrod\n      showIcons: false\n    yaml:\n      keyColor: red\n      colonColor: white\n      valueColor: red\n    logs:\n      fgColor: white\n      bgColor: black\n      indicator:\n        fgColor: red\n        bgColor: black\n        toggleOnColor: red\n        toggleOffColor: white\n"
  },
  {
    "path": "skins/rose-pine-dawn.yaml",
    "content": "# -----------------------------------------------------------------------------\n# Rose Pine Dawn\n# https://rosepinetheme.com/palette/ingredients/\n# -----------------------------------------------------------------------------\n#\ntext: &text \"#575279\"\nbase: &base \"#faf4ed\"\noverlay: &overlay \"#f2e9e1\"\nmuted: &muted \"#9893a5\"\nrose: &rose \"#d7827e\"\npine: &pine \"#286983\"\ngold: &gold \"#ea9d34\"\niris: &iris \"#907aa9\"\nlove: &love \"#b4637a\"\n\n# Skin...\nk9s:\n  # General K9s styles\n  body:\n    fgColor: *text\n    bgColor: *base\n    logoColor: *iris\n  # Command prompt styles\n  prompt:\n    fgColor: *text\n    bgColor: *base\n    suggestColor: *iris\n  # ClusterInfoView styles.\n  info:\n    fgColor: *iris\n    sectionColor: *text\n  # Dialog styles.\n  dialog:\n    fgColor: *text\n    bgColor: *base\n    buttonFgColor: *text\n    buttonBgColor: *iris\n    buttonFocusFgColor: *gold\n    buttonFocusBgColor: *iris\n    labelFgColor: *gold\n    fieldFgColor: *text\n  frame:\n    # Borders styles.\n    border:\n      fgColor: *overlay\n      focusColor: *overlay\n    menu:\n      fgColor: *text\n      keyColor: *iris\n      # Used for favorite namespaces\n      numKeyColor: *iris\n    # CrumbView attributes for history navigation.\n    crumbs:\n      fgColor: *text\n      bgColor: *overlay\n      activeColor: *overlay\n    # Resource status and update styles\n    status:\n      newColor: *rose\n      modifyColor: *iris\n      addColor: *pine\n      errorColor: *love\n      highlightcolor: *gold\n      killColor: *muted\n      completedColor: *muted\n    # Border title styles.\n    title:\n      fgColor: *text\n      bgColor: *overlay\n      highlightColor: *gold\n      counterColor: *iris\n      filterColor: *iris\n  views:\n    # Charts skins...\n    charts:\n      bgColor: default\n      defaultDialColors:\n        - *iris\n        - *love\n      defaultChartColors:\n        - *iris\n        - *love\n    # TableView attributes.\n    table:\n      fgColor: *text\n      bgColor: *base\n      # Header row styles.\n      header:\n        fgColor: *text\n        bgColor: *base\n        sorterColor: *rose\n    # Xray view attributes.\n    xray:\n      fgColor: *text\n      bgColor: *base\n      cursorColor: *overlay\n      graphicColor: *iris\n      showIcons: false\n    # YAML info styles.\n    yaml:\n      keyColor: *iris\n      colonColor: *iris\n      valueColor: *text\n    # Logs styles.\n    logs:\n      fgColor: *text\n      bgColor: *base\n      indicator:\n        fgColor: *text\n        bgColor: *iris\n"
  },
  {
    "path": "skins/rose-pine-moon.yaml",
    "content": "# -----------------------------------------------------------------------------\n# Rose Pine Main\n# https://rosepinetheme.com/palette/ingredients/\n# -----------------------------------------------------------------------------\n#\ntext: &text \"#e0def4\"\nbase: &base \"#232136\"\noverlay: &overlay \"#393552\"\nmuted: &muted \"#6e6a86\"\nrose: &rose \"#ea9a97\"\npine: &pine \"#3e8fb0\"\ngold: &gold \"#f6c177\"\niris: &iris \"#c4a7e7\"\nlove: &love \"#eb6f92\"\n\n# Skin...\nk9s:\n  # General K9s styles\n  body:\n    fgColor: *text\n    bgColor: *base\n    logoColor: *iris\n  # Command prompt styles\n  prompt:\n    fgColor: *text\n    bgColor: *base\n    suggestColor: *iris\n  # ClusterInfoView styles.\n  info:\n    fgColor: *iris\n    sectionColor: *text\n  # Dialog styles.\n  dialog:\n    fgColor: *text\n    bgColor: *base\n    buttonFgColor: *text\n    buttonBgColor: *iris\n    buttonFocusFgColor: *gold\n    buttonFocusBgColor: *iris\n    labelFgColor: *gold\n    fieldFgColor: *text\n  frame:\n    # Borders styles.\n    border:\n      fgColor: *overlay\n      focusColor: *overlay\n    menu:\n      fgColor: *text\n      keyColor: *iris\n      # Used for favorite namespaces\n      numKeyColor: *iris\n    # CrumbView attributes for history navigation.\n    crumbs:\n      fgColor: *text\n      bgColor: *overlay\n      activeColor: *overlay\n    # Resource status and update styles\n    status:\n      newColor: *rose\n      modifyColor: *iris\n      addColor: *pine\n      errorColor: *love\n      highlightcolor: *gold\n      killColor: *muted\n      completedColor: *muted\n    # Border title styles.\n    title:\n      fgColor: *text\n      bgColor: *overlay\n      highlightColor: *gold\n      counterColor: *iris\n      filterColor: *iris\n  views:\n    # Charts skins...\n    charts:\n      bgColor: default\n      defaultDialColors:\n        - *iris\n        - *love\n      defaultChartColors:\n        - *iris\n        - *love\n    # TableView attributes.\n    table:\n      fgColor: *text\n      bgColor: *base\n      # Header row styles.\n      header:\n        fgColor: *text\n        bgColor: *base\n        sorterColor: *rose\n    # Xray view attributes.\n    xray:\n      fgColor: *text\n      bgColor: *base\n      cursorColor: *overlay\n      graphicColor: *iris\n      showIcons: false\n    # YAML info styles.\n    yaml:\n      keyColor: *iris\n      colonColor: *iris\n      valueColor: *text\n    # Logs styles.\n    logs:\n      fgColor: *text\n      bgColor: *base\n      indicator:\n        fgColor: *text\n        bgColor: *iris\n"
  },
  {
    "path": "skins/rose-pine.yaml",
    "content": "# -----------------------------------------------------------------------------\n# Rose Pine skin\n# -----------------------------------------------------------------------------\n\n# Styles...\nforeground: &foreground \"#e0def4\"\nbackground: &background \"#191724\"\ncurrent_line: &current_line \"#26233a\"\nselection: &selection \"#26233a\"\ncomment: &comment \"#6272a4\"\ncyan: &cyan \"#ebbcba\"\ngreen: &green \"#31748f\"\norange: &orange \"#f6c177\"\npink: &pink \"#c4a7e7\"\npurple: &purple \"#c4a7e7\"\nred: &red \"#eb6f92\"\nyellow: &yellow \"#f6c177\"\n\n# Skin...\nk9s:\n  # General K9s styles\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *purple\n  # Command prompt styles\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *purple\n  # ClusterInfoView styles.\n  info:\n    fgColor: *pink\n    sectionColor: *foreground\n  # Dialog styles.\n  dialog:\n    fgColor: *foreground\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *purple\n    buttonFocusFgColor: *yellow\n    buttonFocusBgColor: *pink\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  frame:\n    # Borders styles.\n    border:\n      fgColor: *selection\n      focusColor: *current_line\n    menu:\n      fgColor: *foreground\n      keyColor: *pink\n      # Used for favorite namespaces\n      numKeyColor: *pink\n    # CrumbView attributes for history navigation.\n    crumbs:\n      fgColor: *foreground\n      bgColor: *current_line\n      activeColor: *current_line\n    # Resource status and update styles\n    status:\n      newColor: *cyan\n      modifyColor: *purple\n      addColor: *green\n      errorColor: *red\n      highlightColor: *orange\n      killColor: *comment\n      completedColor: *comment\n    # Border title styles.\n    title:\n      fgColor: *foreground\n      bgColor: *current_line\n      highlightColor: *orange\n      counterColor: *purple\n      filterColor: *pink\n  views:\n    # Charts skins...\n    charts:\n      bgColor: default\n      defaultDialColors:\n        - *purple\n        - *red\n      defaultChartColors:\n        - *purple\n        - *red\n    # TableView attributes.\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      # Header row styles.\n      header:\n        fgColor: *foreground\n        bgColor: *background\n        sorterColor: *cyan\n    # Xray view attributes.\n    xray:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *current_line\n      graphicColor: *purple\n      showIcons: false\n    # YAML info styles.\n    yaml:\n      keyColor: *pink\n      colonColor: *purple\n      valueColor: *foreground\n    # Logs styles.\n    logs:\n      fgColor: *foreground\n      bgColor: *background\n      indicator:\n        fgColor: *foreground\n        bgColor: *purple\n        toggleOnColor: *green\n        toggleOffColor: *selection\n"
  },
  {
    "path": "skins/snazzy.yaml",
    "content": "# -----------------------------------------------------------------------------\n# Snazzy skin\n# -----------------------------------------------------------------------------\n\n# Skin...\nk9s:\n  body:\n    fgColor: \"#97979b\"\n    bgColor: \"#282a36\"\n    logoColor: \"#5af78e\"\n  prompt:\n    fgColor: \"#97979b\"\n    bgColor: \"#282a36\"\n    suggestColor: \"#5af78e\"\n  info:\n    fgColor: white\n    sectionColor: \"#5af78e\"\n  dialog:\n    fgColor: \"#97979b\"\n    bgColor: \"#282a36\"\n    buttonFgColor: \"#97979b\"\n    buttonBgColor: \"#282a36\"\n    buttonFocusFgColor: \"#97979b\"\n    buttonFocusBgColor: \"#5af78e\"\n    labelFgColor: \"#97979b\"\n    fieldFgColor: \"#5af78e\"\n  frame:\n    border:\n      fgColor: \"#5af78e\"\n      focusColor: \"#5af78e\"\n    menu:\n      fgColor: white\n      keyColor: \"#57c7ff\"\n      numKeyColor: \"#ff6ac1\"\n    crumbs:\n      fgColor: \"#282a36\"\n      bgColor: white\n      activeColor: \"#f3f99d\"\n    status:\n      newColor: \"#eff0eb\"\n      modifyColor: \"#5af78e\"\n      addColor: \"#57c7ff\"\n      errorColor: \"#ff5c57\"\n      highlightColor: \"#f3f99d\"\n      killColor: mediumpurple\n      completedColor: gray\n    title:\n      fgColor: \"#5af78e\"\n      bgColor: \"#282a36\"\n      highlightColor: white\n      counterColor: white\n      filterColor: \"#57c7ff\"\n  views:\n    # Charts skins...\n    charts:\n      bgColor: default\n      defaultDialColors:\n        - \"#57c7ff\"\n        - \"#ff5c57\"\n      defaultChartColors:\n        - \"#57c7ff\"\n        - \"#ff5c57\"\n    table:\n      fgColor: \"#57c7ff\"\n      bgColor: \"#282a36\"\n      markColor: darkgoldenrod\n      header:\n        fgColor: white\n        bgColor: \"#282a36\"\n        sorterColor: orange\n    xray:\n      fgColor: \"#57c7ff\"\n      bgColor: \"#282a36\"\n      cursorColor: \"#5af78e\"\n      graphicColor: darkgoldenrod\n      showIcons: false\n    yaml:\n      keyColor: \"#ff5c57\"\n      colonColor: white\n      valueColor: \"#f3f99d\"\n    logs:\n      fgColor: white\n      bgColor: \"#282a36\"\n      indicator:\n        fgColor: white\n        bgColor: \"#282a36\"\n        toggleOnColor: \"#ff5c57\"\n        toggleOffColor: white\n"
  },
  {
    "path": "skins/solarized-16.yaml",
    "content": "# K9s Solarized Skin Contributed by [@graelo](graelo@grael.cc)\n#\n# The table below is extracted from <https://github.com/gdamore/tcell/blob/bd74010edcb3e8e36c090323c2b1d4a6b182d487/color.go#L846-L861>\n# and joined with both the ascii standard names from <https://en.wikipedia.org/wiki/ANSI_escape_code#Colors>\n# and the solarized color names from <https://ethanschoonover.com/solarized/>\n#\n#\t\"black\":                ColorBlack,        black            base02\n#\t\"maroon\":               ColorMaroon,       red              red\n#\t\"green\":                ColorGreen,        green            green\n#\t\"olive\":                ColorOlive,        yellow           yellow\n#\t\"navy\":                 ColorNavy,         blue             blue\n#\t\"purple\":               ColorPurple,       magenta          magenta\n#\t\"teal\":                 ColorTeal,         cyan             cyan\n#\t\"silver\":               ColorSilver,       white            base2\n#\t\"gray\":                 ColorGray,         brightblack      base03\n#\t\"red\":                  ColorRed,          brightred        orange\n#\t\"lime\":                 ColorLime,         brightgreen      base01\n#\t\"yellow\":               ColorYellow,       brightyellow     base00\n#\t\"blue\":                 ColorBlue,         brightblue       base0\n#\t\"fuchsia\":              ColorFuchsia,      brightmagenta    violet\n#\t\"aqua\":                 ColorAqua,         brightcyan       base1\n#\t\"white\":                ColorWhite,        brightwhite      base3\n\nbase03:       &base03   gray          # base03    brightblack\nbase02:       &base02   black         # base02    black\nbase01:       &base01   lime          # base01    brightgreen\nbase00:       &base00   yellow        # base00    brightyellow\nbase0:        &base0    blue          # base0     brightblue\nbase1:        &base1    aqua          # base1     brightcyan\nbase2:        &base2    silver        # base2     white\nbase3:        &base3    white         # base3     brightwhite\nyellow:       &yellow   olive         # accent    yellow        #b58900\norange:       &orange   red           # accent    orange        #cb4b16\nred:          &red      maroon        # accent    red           #dc322f\nmagenta:      &magenta  purple        # accent    magenta       #d33682\nviolet:       &violet   fuchsia       # accent    violet        #6c71c4\nblue:         &blue     navy          # accent    blue          #268bd2\ncyan:         &cyan     teal          # accent    cyan          #2aa198\ngreen:        &green    green         # accent    green         #859900\n\nbackground:   &background   default   # transparent\nforeground:   &foreground   yellow    # base00\ncurrent_line: &current_line white     # base2\nselection:    &selection    silver    # base2\ncomment:      &comment      aqua      # base1\n\nk9s:\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *magenta\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *orange\n  info:\n    fgColor: *blue\n    sectionColor: *foreground\n  dialog:\n    fgColor: *foreground\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *magenta\n    buttonFocusFgColor: *base2\n    buttonFocusBgColor: *cyan\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  frame:\n    border:\n      fgColor: *selection\n      focusColor: *foreground\n    menu:\n      fgColor: *foreground\n      keyColor: *blue\n      numKeyColor: *green\n    crumbs:\n      fgColor: *base2\n      bgColor: *base0\n      activeColor: *blue\n    status:\n      newColor: *base00\n      modifyColor: *blue\n      addColor: *yellow\n      errorColor: *red\n      highlightColor: *orange\n      killColor: *violet\n      completedColor: *green\n    title:\n      fgColor: *foreground\n      bgColor: *background\n      highlightColor: *blue\n      counterColor: *magenta\n      filterColor: *magenta\n  views:\n    charts:\n      bgColor: default\n      defaultDialColors:\n        - *blue\n        - *red\n      defaultChartColors:\n        - *blue\n        - *red\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      cursorFgColor: *base2\n      cursorBgColor: *background\n      markColor: *magenta\n      header:\n        fgColor: *foreground\n        bgColor: *background\n        sorterColor: *magenta\n    xray:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *current_line\n      graphicColor: *blue\n      showIcons: false\n    yaml:\n      keyColor: *green\n      colonColor: *base02\n      valueColor: *foreground\n    logs:\n      fgColor: *foreground\n      bgColor: *background\n      indicator:\n        fgColor: *foreground\n        bgColor: *selection\n"
  },
  {
    "path": "skins/solarized-dark.yaml",
    "content": "# -----------------------------------------------------------------------------\n# Solarized Dark Skin\n# Based on: K9s Solarized Dark Skin\n# Author:   [@danmikita](danmikita@gmail.com)\n# -----------------------------------------------------------------------------\n\n# Styles...\nforeground: &foreground \"#839495\"\nbackground: &background \"#002833\"\ncurrent_line: &current_line \"#003440\"\nselection: &selection \"#003440\"\ncomment: &comment \"#6272a4\"\ncyan: &cyan \"#2aa197\"\ngreen: &green \"#859901\"\norange: &orange \"#cb4a16\"\nmagenta: &magenta \"#d33582\"\nblue: &blue \"#2aa198\"\nred: &red \"#dc312e\"\n\n# Skin...\nk9s:\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *blue\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *orange\n  info:\n    fgColor: *magenta\n    sectionColor: *foreground\n  dialog:\n    fgColor: *foreground\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *magenta\n    buttonFocusFgColor: white\n    buttonFocusBgColor: *cyan\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  frame:\n    border:\n      fgColor: *selection\n      focusColor: *current_line\n    menu:\n      fgColor: *foreground\n      keyColor: *magenta\n      numKeyColor: *magenta\n    crumbs:\n      fgColor: *foreground\n      bgColor: *current_line\n      activeColor: *current_line\n    status:\n      newColor: *cyan\n      modifyColor: *blue\n      addColor: *green\n      errorColor: *red\n      highlightColor: *orange\n      killColor: *comment\n      completedColor: *comment\n    title:\n      fgColor: *foreground\n      bgColor: *current_line\n      highlightColor: *orange\n      counterColor: *blue\n      filterColor: *magenta\n  views:\n    charts:\n      bgColor: default\n      defaultDialColors:\n        - *blue\n        - *red\n      defaultChartColors:\n        - *blue\n        - *red\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      cursorFgColor: *selection\n      cursorBgColor: *current_line\n      header:\n        fgColor: *foreground\n        bgColor: *background\n        sorterColor: *cyan\n    xray:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *current_line\n      graphicColor: *blue\n      showIcons: false\n    yaml:\n      keyColor: *magenta\n      colonColor: *blue\n      valueColor: *foreground\n    logs:\n      fgColor: *foreground\n      bgColor: *background\n      indicator:\n        fgColor: *foreground\n        bgColor: *selection\n        toggleOnColor: *magenta\n        toggleOffColor: *blue\n"
  },
  {
    "path": "skins/solarized-light.yaml",
    "content": "# -----------------------------------------------------------------------------\n# Solarized Light Skin\n# Author: [@leg100](louisgarman@gmail.com)\n# -----------------------------------------------------------------------------\n\n# Styles...\nforeground: &foreground \"#657b83\"\nbackground: &background \"#fdf6e3\"\ncurrent_line: &current_line \"#eee8d5\"\nselection: &selection \"#eee8d5\"\ncomment: &comment \"#93a1a1\"\ncyan: &cyan \"#2aa198\"\ngreen: &green \"#859900\"\nyellow: &yellow \"#b58900\"\norange: &orange \"#cb4b16\"\nmagenta: &magenta \"#d33682\"\nblue: &blue \"#268bd2\"\nred: &red \"#dc322f\"\n\n# Skin...\nk9s:\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *blue\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *orange\n  info:\n    fgColor: *magenta\n    sectionColor: *foreground\n  dialog:\n    fgColor: *foreground\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *magenta\n    buttonFocusFgColor: white\n    buttonFocusBgColor: *cyan\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  frame:\n    border:\n      fgColor: *selection\n      focusColor: *foreground\n    menu:\n      fgColor: *foreground\n      keyColor: *magenta\n      numKeyColor: *magenta\n    crumbs:\n      fgColor: white\n      bgColor: *cyan\n      activeColor: *yellow\n    status:\n      newColor: *cyan\n      modifyColor: *blue\n      addColor: *green\n      errorColor: *red\n      highlightColor: *orange\n      killColor: *comment\n      completedColor: *comment\n    title:\n      fgColor: *foreground\n      bgColor: *background\n      highlightColor: *blue\n      counterColor: *magenta\n      filterColor: *magenta\n  views:\n    charts:\n      bgColor: default\n      defaultDialColors:\n        - *blue\n        - *red\n      defaultChartColors:\n        - *blue\n        - *red\n    table:\n      fgColor: *foreground\n      bgColor: *background\n      cursorFgColor: white\n      cursorBgColor: *background\n      markColor: darkgoldenrod\n      header:\n        fgColor: *foreground\n        bgColor: *background\n        sorterColor: *cyan\n    xray:\n      fgColor: *foreground\n      bgColor: *background\n      cursorColor: *current_line\n      graphicColor: *blue\n      showIcons: false\n    yaml:\n      keyColor: *magenta\n      colonColor: *blue\n      valueColor: *foreground\n    logs:\n      fgColor: *foreground\n      bgColor: *background\n      indicator:\n        fgColor: *foreground\n        bgColor: *selection\n        toggleOnColor: *magenta\n        toggleOffColor: *blue\n"
  },
  {
    "path": "skins/stock.yaml",
    "content": "# -----------------------------------------------------------------------------\n# Stock skin\n# -----------------------------------------------------------------------------\n\n# Skin...\nk9s:\n  body:\n    fgColor: dodgerblue\n    bgColor: black\n    logoColor: orange\n  prompt:\n    fgColor: cadetblue\n    bgColor: black\n    suggestColor: dodgerblue\n  info:\n    fgColor: orange\n    sectionColor: white\n  dialog:\n    fgColor: dodgerblue\n    bgColor: black\n    buttonFgColor: black\n    buttonBgColor: dodgerblue\n    buttonFocusFgColor: white\n    buttonFocusBgColor: fuchsia\n    labelFgColor: fuchsia\n    fieldFgColor: dodgerblue\n  frame:\n    border:\n      fgColor: dodgerblue\n      focusColor: aqua\n    menu:\n      fgColor: white\n      fgStyle: dim\n      keyColor: dodgerblue\n      numKeyColor: fuchsia\n    crumbs:\n      fgColor: black\n      bgColor: steelblue\n      activeColor: orange\n    status:\n      newColor: lightskyblue\n      modifyColor: greenyellow\n      addColor: white\n      errorColor: orangered\n      pendingColor: darkorange\n      highlightColor: aqua\n      killColor: mediumpurple\n      completedColor: gray\n    title:\n      fgColor: aqua\n      highlightColor: fuchsia\n      counterColor: papayawhip\n      filterColor: steelblue\n  views:\n    # Charts skins...\n    charts:\n      bgColor: black\n      defaultDialColors:\n        - linegreen\n        - orangered\n      defaultChartColors:\n        - linegreen\n        - orangered\n    table:\n      fgColor: blue\n      bgColor: black\n      cursorFgColor: black\n      cursorBgColor: aqua\n      markColor: darkgoldenrod\n      header:\n        fgColor: white\n        bgColor: black\n        sorterColor: orange\n    xray:\n      fgColor: blue\n      bgColor: black\n      cursorColor: aqua\n      graphicColor: darkgoldenrod\n      showIcons: false\n    yaml:\n      keyColor: steelblue\n      colonColor: white\n      valueColor: papayawhip\n    logs:\n      fgColor: white\n      bgColor: black\n      indicator:\n        fgColor: dodgerblue\n        bgColor: black\n        toggleOnColor: papayawhip\n        toggleOffColor: steelblue\n"
  },
  {
    "path": "skins/transparent.yaml",
    "content": "# -----------------------------------------------------------------------------\n# Transparent skin\n# Preserve your terminal session background color\n# -----------------------------------------------------------------------------\n\n# Skin...\nk9s:\n  body:\n    bgColor: default\n  prompt:\n    bgColor: default\n  info:\n    sectionColor: default\n  dialog:\n    bgColor: default\n    labelFgColor: default\n    fieldFgColor: default\n  frame:\n    crumbs:\n      bgColor: default\n    title:\n      bgColor: default\n      counterColor: default\n    menu:\n      fgColor: default\n  views:\n    charts:\n      bgColor: default\n    table:\n      bgColor: default\n      header:\n        fgColor: default\n        bgColor: default\n    xray:\n      bgColor: default\n    logs:\n      bgColor: default\n      indicator:\n        bgColor: default\n        toggleOnColor: default\n        toggleOffColor: default\n    yaml:\n      colonColor: default\n      valueColor: default\n"
  },
  {
    "path": "skins/vercel.yaml",
    "content": "foreground: &foreground \"#ffffff\"\nbackground: &background \"#000000\"\ncurrent_line: &current_line \"#1a1a1a\"\nselection: &selection \"#e63946\"\ncomment: &comment \"#555555\"\ncyan: &cyan \"#00bcd4\"\ngreen: &green \"#2ecc71\"\norange: &orange \"#f4a261\"\nmagenta: &magenta \"#9d0191\"\nblue: &blue \"#0070f3\"\nred: &red \"#e63946\"\n\nk9s:\n  body:\n    fgColor: *foreground\n    bgColor: *background\n    logoColor: *red\n  prompt:\n    fgColor: *foreground\n    bgColor: *background\n    suggestColor: *red\n  info:\n    fgColor: *red\n    sectionColor: *foreground\n  help:\n    fgColor: *foreground\n    bgColor: *background\n    keyColor: *red\n    numKeyColor: *blue\n    sectionColor: *green\n  dialog:\n    fgColor: *foreground\n    bgColor: *background\n    buttonFgColor: *foreground\n    buttonBgColor: *red\n    buttonFocusFgColor: *background\n    buttonFocusBgColor: *red\n    labelFgColor: *orange\n    fieldFgColor: *foreground\n  frame:\n    border:\n      fgColor: *selection\n      focusColor: *current_line\n    menu:\n      fgColor: *foreground\n      keyColor: *red\n      numKeyColor: *red\n    crumbs:\n      fgColor: *foreground\n      bgColor: *comment\n      activeColor: *red\n    status:\n      newColor: *cyan\n      modifyColor: *blue\n      addColor: *green\n      errorColor: *red\n      highlightColor: *orange"
  },
  {
    "path": "snap/snapcraft.yaml",
    "content": "name: k9s\nbase: core22\nversion: 'v0.50.18'\nsummary: K9s is a CLI to view and manage your Kubernetes clusters.\ndescription: |\n  K9s is a CLI to view and manage your Kubernetes clusters. By leveraging a terminal UI, you can easily traverse Kubernetes resources and view the state of your clusters in a single powerful session.\n\ngrade: stable\nconfinement: classic\n\narchitectures:\n  - amd64\n  - arm64\n  - armhf\n  - i386\n\napps:\n  k9s:\n    command: bin/k9s\n\nparts:\n  build:\n    plugin: go\n    source: https://github.com/derailed/k9s\n    source-type: git\n    source-tag: $SNAPCRAFT_PROJECT_VERSION\n    override-build: |\n      make test\n      make build\n      install $SNAPCRAFT_PART_BUILD/execs/k9s -D $SNAPCRAFT_PART_INSTALL/bin/k9s\n    build-packages:\n      - build-essential\n    build-snaps:\n      - go\n"
  },
  {
    "path": "testdata/aliases/aliases.yaml",
    "content": "aliases:\n  dp: deployments\n  sec: v1/secrets\n  jo: jobs\n  cr: clusterroles\n  crb: clusterrolebindings\n  ro: roles\n  rb: rolebindings\n  np: networkpolicies\n"
  }
]