diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index bbc69f3ac9..8a4d6fdacb 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -14,17 +14,20 @@ assignees: ''

- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error +**Historical Documents** +When applicable please include any supporting artifacts: k9s debug logs, configurations, resource manifests, ... + **Expected behavior** A clear and concise description of what you expected to happen. @@ -32,9 +35,10 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Versions (please complete the following information):** - - OS: [e.g. OSX] - - K9s: [e.g. 0.1.0] - - K8s: [e.g. 1.11.0] + +- OS: [e.g. OSX] +- K9s: [e.g. 0.1.0] +- K8s: [e.g. 1.11.0] **Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md index 30d47509ee..397760147b 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -13,9 +13,8 @@ assignees: ''

- **Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] +A clear and concise description of what the problem is. **Describe the solution you'd like** A clear and concise description of what you want to happen. diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000000..17570b8e46 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,24 @@ +name: K9s Lint + +on: + pull_request: + branches: [ main ] + +jobs: + golangci: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4.1.6 + + - name: Install Go + uses: actions/setup-go@v5.0.1 + with: + go-version-file: go.mod + cache-dependency-path: go.sum + + - name: Lint + uses: golangci/golangci-lint-action@v6.1.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + reporter: github-pr-check \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6133c17697..8b229ce36c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: K9s Checks +name: K9s Test on: workflow_dispatch: @@ -13,13 +13,13 @@ on: - master jobs: build: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - name: Checkout Code - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.1.6 - name: Install Go - uses: actions/setup-go@v4.1.0 + uses: actions/setup-go@v5.0.1 with: go-version-file: go.mod cache-dependency-path: go.sum diff --git a/.gitignore b/.gitignore index 0fc4383608..80b1e5a991 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ .envrc cov.out execs -k9s +/k9s /k8s dist notes @@ -21,4 +21,6 @@ faas .settings/* demos /code -kind \ No newline at end of file +kind +*.snap +/stresser diff --git a/.golangci.yml b/.golangci.yml index a13a58d5c3..f880f84a8d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,13 +1,10 @@ -# This file contains all available configuration options -# with their default values. - # options for analysis running run: # default concurrency is a available CPU number - concurrency: 4 + concurrency: 8 # timeout for analysis, e.g. 30s, 5m, default is 1m - timeout: 1m + timeout: 5m # exit code when at least one issue was found, default is 1 issues-exit-code: 1 @@ -15,33 +12,17 @@ run: # include test files or not, default is true tests: true - # list of build tags, all linters use it. Default is empty list. - build-tags: - - mytag - - # which dirs to skip: issues from them won't be reported; - # can use regexp here: generated.*, regexp is applied on full path; - # default value is empty list, but default dirs are skipped independently - # from this option's value (see skip-dirs-use-default). - # "/" will be replaced by current OS file path separator to properly work - # on Windows. - skip-dirs: - - src/external_libs - - autogenerated_by_my_lib - # default is true. Enables skipping of directories: # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ skip-dirs-use-default: true - # which files to skip: they will be analyzed, but issues from them - # won't be reported. Default value is empty list, but there is - # no need to include all autogenerated files, we confidently recognize - # autogenerated files. If it's not please let us know. - # "/" will be replaced by current OS file path separator to properly work - # on Windows. - skip-files: - - ".*\\.my\\.go$" - - lib/bad.go + # which dirs to skip: they won't be analyzed; + # can use regexp here: generated.*, regexp is applied on full path; + # default value is empty list, but next dirs are always skipped independently + # from this option's value: + # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ + # skip-dirs: + # - ^test.* # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": # If invoked with -mod=readonly, the go command is disallowed from the implicit @@ -51,732 +32,97 @@ run: # If invoked with -mod=vendor, the go command assumes that the vendor # directory holds the correct copies of dependencies and ignores # the dependency descriptions in go.mod. - # modules-download-mode: readonly|vendor|mod - - # Allow multiple parallel golangci-lint instances running. - # If false (default) - golangci-lint acquires file lock on start. - allow-parallel-runners: false - -# output configuration options -output: - # colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions - # default is "colored-line-number" - format: colored-line-number - - # print lines of code with issue, default is true - print-issued-lines: true - - # print linter name in the end of issue text, default is true - print-linter-name: true - - # make issues output unique by line, default is true - uniq-by-line: true - - # add a prefix to the output file references; default is no prefix - path-prefix: "" + modules-download-mode: readonly - # sorts results by: filepath, line and column - sort-results: false + # which files to skip: they will be analyzed, but issues from them + # won't be reported. Default value is empty list, but there is + # no need to include all autogenerated files, we confidently recognize + # autogenerated files. If it's not please let us know. + skip-files: + # - ".*\\.my\\.go$" + # - lib/bad.go # all available settings of specific linters linters-settings: - cyclop: - # the maximal code complexity to report - max-complexity: 20 - # the maximal average package complexity. If it's higher than 0.0 (float) the check is enabled (default 0.0) - package-average: 0.0 - # should ignore tests (default false) - skip-tests: false - - dogsled: - # checks assignments with too many blank identifiers; default is 2 - max-blank-identifiers: 2 - - dupl: - # tokens count to trigger issue, 150 by default - threshold: 100 - - errcheck: - # report about not checking of errors in type assertions: `a := b.(MyStruct)`; - # default is false: such cases aren't reported by default. - check-type-assertions: false - - # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; - # default is false: such cases aren't reported by default. - check-blank: false - - # [deprecated] comma-separated list of pairs of the form pkg:regex - # the regex is used to ignore names within pkg. (default "fmt:.*"). - # see https://github.com/kisielk/errcheck#the-deprecated-method for details - ignore: fmt:.*,io/ioutil:^Read.* - - # path to a file containing a list of functions to exclude from checking - # see https://github.com/kisielk/errcheck#excluding-functions for details - # exclude: /path/to/file.txt - - errorlint: - # Check whether fmt.Errorf uses the %w verb for formatting errors. See the readme for caveats - errorf: true - # Check for plain type assertions and type switches - asserts: true - # Check for plain error comparisons - comparison: true - - exhaustive: - # check switch statements in generated files also - check-generated: false - # indicates that switch statements are to be considered exhaustive if a - # 'default' case is present, even if all enum members aren't listed in the - # switch - default-signifies-exhaustive: false - - exhaustivestruct: - # Struct Patterns is list of expressions to match struct packages and names - # The struct packages have the form example.com/package.ExampleStruct - # The matching patterns can use matching syntax from https://pkg.go.dev/path#Match - # If this list is empty, all structs are tested. - struct-patterns: - - "*.Test" - - "example.com/package.ExampleStruct" - - forbidigo: - # Forbid the following identifiers (identifiers are written using regexp): - forbid: - - ^print.*$ - - 'fmt\.Print.*' - # Exclude godoc examples from forbidigo checks. Default is true. - exclude_godoc_examples: false - - funlen: - lines: 100 - statements: 40 - - gci: - # put imports beginning with prefix after 3rd-party packages; - # only support one prefix - # if not set, use goimports.local-prefixes - local-prefixes: github.com/org/project - - gocognit: - # minimal code complexity to report, 30 by default (but we recommend 10-20) - min-complexity: 10 - - nestif: - # minimal complexity of if statements to report, 5 by default - min-complexity: 4 - - goconst: - # minimal length of string constant, 3 by default - min-len: 3 - # minimal occurrences count to trigger, 3 by default - min-occurrences: 3 - - gocritic: - # Which checks should be enabled; can't be combined with 'disabled-checks'; - # See https://go-critic.github.io/overview#checks-overview - # To check which checks are enabled run `GL_DEBUG=gocritic golangci-lint run` - # By default list of stable checks is used. - # enabled-checks: - # - rangeValCopy - - # Which checks should be disabled; can't be combined with 'enabled-checks'; default is empty - disabled-checks: - - regexpMust - - # Enable multiple checks by tags, run `GL_DEBUG=gocritic golangci-lint run` to see all tags and checks. - # Empty list by default. See https://github.com/go-critic/go-critic#usage -> section "Tags". - enabled-tags: - - performance - disabled-tags: - - experimental - - # Settings passed to gocritic. - # The settings key is the name of a supported gocritic checker. - # The list of supported checkers can be find in https://go-critic.github.io/overview. - settings: - captLocal: # must be valid enabled check name - # whether to restrict checker to params only (default true) - paramsOnly: true - elseif: - # whether to skip balanced if-else pairs (default true) - skipBalanced: true - hugeParam: - # size in bytes that makes the warning trigger (default 80) - sizeThreshold: 80 - # nestingReduce: - # # min number of statements inside a branch to trigger a warning (default 5) - # bodyWidth: 5 - rangeExprCopy: - # size in bytes that makes the warning trigger (default 512) - sizeThreshold: 512 - # whether to check test functions (default true) - skipTestFuncs: true - rangeValCopy: - # size in bytes that makes the warning trigger (default 128) - sizeThreshold: 32 - # whether to check test functions (default true) - skipTestFuncs: true - # ruleguard: - # path to a gorules file for the ruleguard checker - # rules: "" - # truncateCmp: - # # whether to skip int/uint/uintptr types (default true) - # skipArchDependent: true - underef: - # whether to skip (*x).method() calls where x is a pointer receiver (default true) - skipRecvDeref: true - # unnamedResult: - # # whether to check exported functions - # checkExported: true - gocyclo: - # minimal code complexity to report, 30 by default (but we recommend 10-20) - min-complexity: 20 - - godot: - # comments to be checked: `declarations`, `toplevel`, or `all` - scope: declarations - # list of regexps for excluding particular comment lines from check - exclude: - # example: exclude comments which contain numbers - # - '[0-9]+' - # check that each sentence starts with a capital letter - capital: false - - godox: - # report any comments starting with keywords, this is useful for TODO or FIXME comments that - # might be left in the code accidentally and should be resolved before merging - keywords: # default keywords are TODO, BUG, and FIXME, these can be overwritten by this setting - - NOTE - - OPTIMIZE # marks code that should be optimized before merging - - HACK # marks hack-arounds that should be removed before merging - - gofmt: - # simplify code: gofmt with `-s` option, true by default - simplify: true - - gofumpt: - # Choose whether or not to use the extra rules that are disabled - # by default - extra-rules: false - - # goheader: - # values: - # const: - # define here const type values in format k:v, for example: - # COMPANY: MY COMPANY - # regexp: - # define here regexp type values, for example - # AUTHOR: .*@mycompany\.com - # template:# |- - # put here copyright header template for source code files, for example: - # Note: {{ YEAR }} is a builtin value that returns the year relative to the current machine time. - # - # {{ AUTHOR }} {{ COMPANY }} {{ YEAR }} - # SPDX-License-Identifier: Apache-2.0 - # Licensed under the Apache License, Version 2.0 (the "License"); - # you may not use this file except in compliance with the License. - # You may obtain a copy of the License at: - # http://www.apache.org/licenses/LICENSE-2.0 - # Unless required by applicable law or agreed to in writing, software - # distributed under the License is distributed on an "AS IS" BASIS, - # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - # See the License for the specific language governing permissions and - # limitations under the License. - # template-path: - # also as alternative of directive 'template' you may put the path to file with the template source - - goimports: - # put imports beginning with prefix after 3rd-party packages; - # it's a comma-separated list of prefixes - local-prefixes: github.com/org/project - - golint: - # minimal confidence for issues, default is 0.8 - min-confidence: 0.8 - - gomnd: - settings: - mnd: - # the list of enabled checks, see https://github.com/tommy-muehle/go-mnd/#checks for description. - checks: - - argument - - case - - condition - - operation - - return - - assign - # ignored-numbers: 1000 - # ignored-files: magic_.*.go - # ignored-functions: math.* - - gomoddirectives: - # Allow local `replace` directives. Default is false. - replace-local: false - # List of allowed `replace` directives. Default is empty. - replace-allow-list: - - launchpad.net/gocheck - # Allow to not explain why the version has been retracted in the `retract` directives. Default is false. - retract-allow-no-explanation: false - # Forbid the use of the `exclude` directives. Default is false. - exclude-forbidden: false - - gomodguard: - # allowed: - # modules: # List of allowed modules - # - gopkg.in/yaml.v2 - # domains:# List of allowed module domains - # - golang.org - # blocked: - # modules: # List of blocked modules - # - github.com/uudashr/go-module: # Blocked module - # recommendations: # Recommended modules that should be used instead (Optional) - # - golang.org/x/mod - # reason: "`mod` is the official go.mod parser library." # Reason why the recommended module should be used (Optional) - # versions:# List of blocked module version constraints - # - github.com/mitchellh/go-homedir: # Blocked module with version constraint - # version: "< 1.1.0" # Version constraint, see https://github.com/Masterminds/semver#basic-comparisons - # reason: "testing if blocked version constraint works." # Reason why the version constraint exists. (Optional) - local_replace_directives: false # Set to true to raise lint issues for packages that are loaded from a local path via replace directive - - gosec: - # To select a subset of rules to run. - # Available rules: https://github.com/securego/gosec#available-rules - includes: - - G401 - - G306 - - G101 - # To specify a set of rules to explicitly exclude. - # Available rules: https://github.com/securego/gosec#available-rules - excludes: - - G204 - # To specify the configuration of rules. - # The configuration of rules is not fully documented by gosec: - # https://github.com/securego/gosec#configuration - # https://github.com/securego/gosec/blob/569328eade2ccbad4ce2d0f21ee158ab5356a5cf/rules/rulelist.go#L60-L102 - config: - G306: "0600" - G101: - pattern: "(?i)example" - ignore_entropy: false - entropy_threshold: "80.0" - per_char_threshold: "3.0" - truncate: "32" - - gosimple: - # Select the Go version to target. The default is '1.13'. - go: "1.20" - # https://staticcheck.io/docs/options#checks - checks: ["all"] + # minimal code complexity to report, 30 by default (but we recommend 10-20) + min-complexity: 35 govet: - # report about shadowed variables - check-shadowing: true - - # settings per analyzer - settings: - printf: # analyzer name, run `go tool vet help` to see all analyzers - funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf - - # enable or disable analyzers by name - # run `go tool vet help` to see all analyzers - # enable: - # - atomicalign - # enable-all: true - # disable: - # - shadow - # disable-all: false - - depguard: - list-type: blacklist - include-go-root: false - packages: - - github.com/sirupsen/logrus - packages-with-error-message: - # specify an error message to output when a blacklisted package is used - - github.com/sirupsen/logrus: "logging is allowed only by logutils.Log" - - ifshort: - # Maximum length of variable declaration measured in number of lines, after which linter won't suggest using short syntax. - # Has higher priority than max-decl-chars. - max-decl-lines: 1 - # Maximum length of variable declaration measured in number of characters, after which linter won't suggest using short syntax. - max-decl-chars: 30 - - importas: - # if set to `true`, force to use alias. - no-unaliased: true - # List of aliases - alias: - # using `servingv1` alias for `knative.dev/serving/pkg/apis/serving/v1` package - - pkg: knative.dev/serving/pkg/apis/serving/v1 - alias: servingv1 - # using `autoscalingv1alpha1` alias for `knative.dev/serving/pkg/apis/autoscaling/v1alpha1` package - - pkg: knative.dev/serving/pkg/apis/autoscaling/v1alpha1 - alias: autoscalingv1alpha1 - # You can specify the package path by regular expression, - # and alias by regular expression expansion syntax like below. - # see https://github.com/julz/importas#use-regular-expression for details - - pkg: knative.dev/serving/pkg/apis/(\w+)/(v[\w\d]+) - alias: $1$2 - - lll: - # max line length, lines longer will be reported. Default is 120. - # '\t' is counted as 1 character by default, and can be changed with the tab-width option - line-length: 120 - # tab width in spaces. Default to 1. - tab-width: 1 - - makezero: - # Allow only slices initialized with a length of zero. Default is false. - always: false - - maligned: - # print struct with more effective memory layout or not, false by default - suggest-new: true - - misspell: - # Correct spellings using locale preferences for US or UK. - # Default is to use a neutral variety of English. - # Setting locale to US will correct the British spelling of 'colour' to 'color'. - locale: US - ignore-words: - - someword - - nakedret: - # make an issue if func has more lines of code than this setting and it has naked returns; default is 30 - max-func-lines: 30 - - prealloc: - # XXX: we don't recommend using this linter before doing performance profiling. - # For most programs usage of prealloc will be a premature optimization. - - # Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them. - # True by default. - simple: true - range-loops: true # Report preallocation suggestions on range loops, true by default - for-loops: false # Report preallocation suggestions on for loops, false by default - - promlinter: - # Promlinter cannot infer all metrics name in static analysis. - # Enable strict mode will also include the errors caused by failing to parse the args. - strict: false - # Please refer to https://github.com/yeya24/promlinter#usage for detailed usage. - disabled-linters: - # - "Help" - # - "MetricUnits" - # - "Counter" - # - "HistogramSummaryReserved" - # - "MetricTypeInName" - # - "ReservedChars" - # - "CamelCase" - # - "lintUnitAbbreviations" - - predeclared: - # comma-separated list of predeclared identifiers to not report on - ignore: "" - # include method names and field names (i.e., qualified names) in checks - q: false - - nolintlint: - # Enable to ensure that nolint directives are all used. Default is true. - allow-unused: false - # Disable to ensure that nolint directives don't have a leading space. Default is true. - allow-leading-space: true - # Exclude following linters from requiring an explanation. Default is []. - allow-no-explanation: [] - # Enable to require an explanation of nonzero length after each nolint directive. Default is false. - require-explanation: true - # Enable to require nolint directives to mention the specific linter being suppressed. Default is false. - require-specific: true - - rowserrcheck: - packages: - - github.com/jmoiron/sqlx - - revive: - # see https://github.com/mgechev/revive#available-rules for details. - ignore-generated-header: true - severity: warning - rules: - - name: indent-error-flow - severity: warning - - name: add-constant - severity: warning - arguments: - - maxLitCount: "3" - allowStrs: '""' - allowInts: "0,1,2" - allowFloats: "0.0,0.,1.0,1.,2.0,2." - + enable: + - nilness + goimports: + local-prefixes: github.com/cilium/cilium staticcheck: - # Select the Go version to target. The default is '1.13'. go: "1.20" - # https://staticcheck.io/docs/options#checks - checks: ["all"] - - stylecheck: - # Select the Go version to target. The default is '1.13'. - go: "1.20" - # https://staticcheck.io/docs/options#checks - checks: - ["all", "-ST1000", "-ST1003", "-ST1016", "-ST1020", "-ST1021", "-ST1022"] - # https://staticcheck.io/docs/options#dot_import_whitelist - dot-import-whitelist: - - fmt - # https://staticcheck.io/docs/options#initialisms - initialisms: - [ - "ACL", - "API", - "ASCII", - "CPU", - "CSS", - "DNS", - "EOF", - "GUID", - "HTML", - "HTTP", - "HTTPS", - "ID", - "IP", - "JSON", - "QPS", - "RAM", - "RPC", - "SLA", - "SMTP", - "SQL", - "SSH", - "TCP", - "TLS", - "TTL", - "UDP", - "UI", - "GID", - "UID", - "UUID", - "URI", - "URL", - "UTF8", - "VM", - "XML", - "XMPP", - "XSRF", - "XSS", - ] - # https://staticcheck.io/docs/options#http_status_code_whitelist - http-status-code-whitelist: ["200", "400", "404", "500"] - - tagliatelle: - # check the struck tag name case - case: - # use the struct field name to check the name of the struct tag - use-field-name: true - rules: - # any struct tag type can be used. - # support string case: `camel`, `pascal`, `kebab`, `snake`, `goCamel`, `goPascal`, `goKebab`, `goSnake`, `upper`, `lower` - json: camel - yaml: camel - xml: camel - bson: camel - avro: snake - mapstructure: kebab - - testpackage: - # regexp pattern to skip files - skip-regexp: (export|internal)_test\.go - - thelper: - # The following configurations enable all checks. It can be omitted because all checks are enabled by default. - # You can enable only required checks deleting unnecessary checks. - test: - first: true - name: true - begin: true - benchmark: - first: true - name: true - begin: true - tb: - first: true - name: true - begin: true - - unparam: - # Inspect exported functions, default is false. Set to true if no external program/library imports your code. - # XXX: if you enable this setting, unparam will report a lot of false-positives in text editors: - # if it's called for subdir of a project it can't find external interfaces. All text editor integrations - # with golangci-lint call it on a directory with the changed file. - check-exported: false - unused: - # Select the Go version to target. The default is '1.13'. go: "1.20" + goheader: + values: + regexp: + PROJECT: 'K9s' + template: |- + SPDX-License-Identifier: Apache-2.0 + Copyright Authors of {{ PROJECT }} + gosec: + includes: + - G402 + gomodguard: + blocked: + modules: + - github.com/miekg/dns: + recommendations: + - github.com/cilium/dns + reason: "use the cilium fork directly to avoid replace directives in go.mod, see https://github.com/cilium/cilium/pull/27582" + - gopkg.in/check.v1: + recommendations: + - testing + - github.com/stretchr/testify/assert + reason: "gocheck has been deprecated, see https://docs.cilium.io/en/latest/contributing/testing/unit/#migrating-tests-off-of-gopkg-in-check-v1" + - go.uber.org/multierr: + recommendations: + - errors + reason: "Go 1.20+ has support for combining multiple errors, see https://go.dev/doc/go1.20#errors" - whitespace: - multi-if: false # Enforces newlines (or comments) after every multi-line if statement - multi-func: false # Enforces newlines (or comments) after every multi-line function signature - - wrapcheck: - # An array of strings that specify substrings of signatures to ignore. - # If this set, it will override the default set of ignored signatures. - # See https://github.com/tomarrell/wrapcheck#configuration for more information. - ignoreSigs: - - .Errorf( - - errors.New( - - errors.Unwrap( - - .Wrap( - - .Wrapf( - - .WithMessage( - - wsl: - # See https://github.com/bombsimon/wsl/blob/master/doc/configuration.md for - # documentation of available settings. These are the defaults for - # `golangci-lint`. - allow-assign-and-anything: false - allow-assign-and-call: true - allow-cuddle-declarations: false - allow-multiline-assign: true - allow-separated-leading-comment: false - allow-trailing-comment: false - force-case-trailing-whitespace: 0 - force-err-cuddling: false - force-short-decl-cuddling: false - strict-append: true - - # The custom section can be used to define linter plugins to be loaded at runtime. - # See README doc for more info. - # custom: - # # Each custom linter should have a unique name. - # example: - # # The path to the plugin *.so. Can be absolute or local. Required for each custom linter - # path: /path/to/example.so - # # The description of the linter. Optional, just for documentation purposes. - # description: This is an example usage of a plugin linter. - # # Intended to point to the repo location of the linter. Optional, just for documentation purposes. - # original-url: github.com/golangci/example-linter +issues: + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + - linters: [staticcheck] + text: "SA1019" # this is rule for deprecated method + - linters: [staticcheck] + text: "SA9003: empty branch" + - linters: [staticcheck] + text: "SA2001: empty critical section" + - linters: [goerr113] + text: "do not define dynamic errors, use wrapped static errors instead" # This rule to avoid opinionated check fmt.Errorf("text") + # Skip goimports check on generated files + - path: \\.(generated\\.deepcopy|pb)\\.go$ + linters: + - goimports + # Skip goheader check on files imported and modified from upstream k8s + - path: "pkg/ipam/(cidrset|service)/.+\\.go" + linters: + - goheader linters: - # disable-all: true + disable-all: true enable: - - megacheck + - goerr113 + - gofmt + - goimports - govet - - funlen + - ineffassign + - misspell + - staticcheck + - unused + - goheader + - gosec + - gomodguard + - gosimple + - errcheck - gocyclo - # - fieldalignment + - gosec + - gosimple + - misspell - prealloc - typecheck - # enable-all: true - disable: - - gosec - # - maligned - # - prealloc - presets: - - bugs - - unused - fast: false - -issues: - # List of regexps of issue texts to exclude, empty list by default. - # But independently from this option we use default exclude patterns, - # it can be disabled by `exclude-use-default: false`. To list all - # excluded by default patterns execute `golangci-lint run --help` - exclude: - - abcdef - - # Excluding configuration per-path, per-linter, per-text and per-source - exclude-rules: - # Exclude some linters from running on tests files. - - path: _test\.go - linters: - - gocyclo - - errcheck - - dupl - - gosec - - funlen - - goconst - - gocognit - - # Exclude known linters from partially hard-vendored code, - # which is impossible to exclude via "nolint" comments. - - path: internal/hmac/ - text: "weak cryptographic primitive" - linters: - - gosec - - # Exclude some staticcheck messages - - linters: - - staticcheck - text: "SA9003:" - - # Exclude lll issues for long lines with go:generate - - linters: - - lll - source: "^//go:generate " - - # Independently from option `exclude` we use default exclude patterns, - # it can be disabled by this option. To list all - # excluded by default patterns execute `golangci-lint run --help`. - # Default value for this option is true. - exclude-use-default: false - - # The default value is false. If set to true exclude and exclude-rules - # regular expressions become case sensitive. - exclude-case-sensitive: false - - # The list of ids of default excludes to include or disable. By default it's empty. - include: - - EXC0002 # disable excluding of issues about comments from golint - - # Maximum issues count per one linter. Set to 0 to disable. Default is 50. - max-issues-per-linter: 0 - - # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. - max-same-issues: 0 - - # Show only new issues: if there are unstaged changes or untracked files, - # only those changes are analyzed, else only changes in HEAD~ are analyzed. - # It's a super-useful option for integration of golangci-lint into existing - # large codebase. It's not practical to fix all existing issues at the moment - # of integration: much better don't allow issues in new code. - # Default is false. - new: false - - # Show only new issues created after git revision `REV` - # new-from-rev: REV - - # Show only new issues created in git patch with set file path. - # new-from-patch: path/to/patch/file - - # Fix found issues (if it's supported by the linter) - fix: true - -severity: - # Default value is empty string. - # Set the default severity for issues. If severity rules are defined and the issues - # do not match or no severity is provided to the rule this will be the default - # severity applied. Severities should match the supported severity names of the - # selected out format. - # - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity - # - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#severity - # - Github: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message - default-severity: error - - # The default value is false. - # If set to true severity-rules regular expressions become case sensitive. - case-sensitive: false - - # Default value is empty list. - # When a list of severity rules are provided, severity information will be added to lint - # issues. Severity rules have the same filtering capability as exclude rules except you - # are allowed to specify one matcher per severity rule. - # Only affects out formats that support setting severity information. - rules: - - linters: - - dupl - severity: info diff --git a/.goreleaser.yml b/.goreleaser.yml index e19e0d4d67..b2ffc71807 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,17 +1,21 @@ project_name: k9s + before: hooks: - go mod download - go generate ./... + release: prerelease: false + +env: + - CGO_ENABLED=0 + builds: - - env: - - CGO_ENABLED=0 + - id: linux goos: - linux - - darwin - - windows + - freebsd goarch: - amd64 - arm64 @@ -23,23 +27,54 @@ builds: flags: - -trimpath ldflags: - - -s -w -X github.com/derailed/k9s/cmd.version=v{{.Version}} -X github.com/derailed/k9s/cmd.commit={{.Commit}} -X github.com/derailed/k9s/cmd.date={{.Date}} + - -s -w -X github.com/derailed/k9s/cmd.version=v{{.Version}} + - -s -w -X github.com/derailed/k9s/cmd.commit={{.Commit}} + - -s -w -X github.com/derailed/k9s/cmd.date={{.Date}} + + - id: osx + goos: + - darwin + goarch: + - amd64 + - arm64 + flags: + - -trimpath + ldflags: + - -s -w -X github.com/derailed/k9s/cmd.version=v{{.Version}} + - -s -w -X github.com/derailed/k9s/cmd.commit={{.Commit}} + - -s -w -X github.com/derailed/k9s/cmd.date={{.Date}} + + - id: windows + goos: + - windows + goarch: + - amd64 + - arm64 + flags: + - -trimpath + ldflags: + - -s -w -X github.com/derailed/k9s/cmd.version=v{{.Version}} + - -s -w -X github.com/derailed/k9s/cmd.commit={{.Commit}} + - -s -w -X github.com/derailed/k9s/cmd.date={{.Date}} + archives: - - name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" - replacements: - darwin: Darwin - linux: Linux - windows: Windows - bit: Arm - bitv6: Arm6 - bitv7: Arm7 + - name_template: >- + {{ .ProjectName }}_ + {{- title .Os }}_ + {{- if eq .Arch "amd64" }}amd64 + {{- else if eq .Arch "386" }}i386 + {{- else }}{{ .Arch }}{{ end }} + {{- if .Arm }}v{{ .Arm }}{{ end }} format_overrides: - goos: windows format: zip + checksum: name_template: "checksums.sha256" + snapshot: name_template: "{{ .Tag }}-next" + changelog: sort: asc filters: @@ -47,10 +82,9 @@ changelog: - "^docs:" - "^test:" -# Homebrew brews: - name: k9s - tap: + repository: owner: derailed name: homebrew-k9s commit_author: @@ -61,3 +95,24 @@ brews: description: Kubernetes CLI To Manage Your Clusters In Style! test: | system "k9s version" + +nfpms: + - file_name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}' + maintainer: Fernand Galiana + homepage: https://k9scli.io + description: Kubernetes CLI To Manage Your Clusters In Style! + license: "Apache-2.0" + formats: + - deb + - rpm + - apk + bindir: /usr/bin + section: utils + contents: + - src: ./LICENSE + dst: /usr/share/doc/k9s/copyright + file_info: + mode: 0644 + +sboms: + - artifacts: archive \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 6018cbedde..2c372a0e8c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # ----------------------------------------------------------------------------- # The base image for building the k9s binary -FROM golang:1.20.4-alpine3.16 AS build +FROM golang:1.22-alpine3.20 AS build WORKDIR /k9s COPY go.mod go.sum main.go Makefile ./ @@ -12,8 +12,8 @@ RUN apk --no-cache add --update make libx11-dev git gcc libc-dev curl && make bu # ----------------------------------------------------------------------------- # Build the final Docker image -FROM alpine:3.18.4 -ARG KUBECTL_VERSION="v1.25.2" +FROM alpine:3.20.0 +ARG KUBECTL_VERSION="v1.29.0" COPY --from=build /k9s/execs/k9s /bin/k9s RUN apk add --update ca-certificates \ diff --git a/Makefile b/Makefile index e59570082b..a2b5a10ba3 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ DATE ?= $(shell TZ=UTC date -j -f "%s" ${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H: else DATE ?= $(shell date -u -d @${SOURCE_DATE_EPOCH} +"%Y-%m-%dT%H:%M:%SZ") endif -VERSION ?= v0.28.2 +VERSION ?= v0.32.5 IMG_NAME := derailed/k9s IMAGE := ${IMG_NAME}:${VERSION} diff --git a/README.md b/README.md index 43ce1db346..37af92c782 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,10 @@ for changes and offers subsequent commands to interact with your observed resour ## Note... -As you may know k9s is not pimped out by a big corporation with deep pockets. It is a complex OSS project that demands a lot of my time to maintain and support. K9s will always remain OSS and therefore free! That said if you feel, k9s makes your day to day Kubernetes journey a tad brighter, please consider sponsoring us or purchase a [K9sAlpha license](https://k9salpha.io). Your donations will go a long way in keeping our servers lights on and beers in our fridge! +K9s is not pimped out by a big corporation with deep pockets. +It is a complex OSS project that demands a lot of my time to maintain and support. +K9s 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) +Your donations will go a long way in keeping our servers lights on and beers in our fridge! **Thank you!** @@ -28,6 +31,35 @@ As you may know k9s is not pimped out by a big corporation with deep pockets. It --- +## Screenshots + +1. Pods + +2. Logs + +3. Deployments + + +--- + +## Demo Videos/Recordings + +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) +* [K9s v0.29.0](https://youtu.be/oiU3wmoAkBo) +* [K9s v0.21.3](https://youtu.be/wG8KCwDAhnw) +* [K9s v0.19.X](https://youtu.be/kj-WverKZ24) +* [K9s v0.18.0](https://www.youtube.com/watch?v=zMnD5e53yRw) +* [K9s v0.17.0](https://www.youtube.com/watch?v=7S33CNLAofk&feature=youtu.be) +* [K9s Pulses](https://asciinema.org/a/UbXKPal6IWpTaVAjBBFmizcGN) +* [K9s v0.15.1](https://youtu.be/7Fx4XQ2ftpM) +* [K9s v0.13.0](https://www.youtube.com/watch?v=qaeR2iK7U0o&t=15s) +* [K9s v0.9.0](https://www.youtube.com/watch?v=bxKfqumjW4I) +* [K9s v0.7.0 Features](https://youtu.be/83jYehwlql8) +* [K9s v0 Demo](https://youtu.be/k7zseUhaXeU) + +--- + ## Documentation Please refer to our [K9s documentation](https://k9scli.io) site for installation, usage, customization and tips. @@ -42,13 +74,12 @@ Wanna discuss K9s features with your fellow `K9sers` or simply show your support ## Installation K9s is available on Linux, macOS and Windows platforms. - -* Binaries for Linux, Windows and Mac are available as tarballs in the [release](https://github.com/derailed/k9s/releases) page. +Binaries for Linux, Windows and Mac are available as tarballs in the [release page](https://github.com/derailed/k9s/releases). * Via [Homebrew](https://brew.sh/) for macOS or Linux ```shell - brew install k9s + brew install derailed/k9s/k9s ``` * Via [MacPorts](https://www.macports.org) @@ -56,6 +87,7 @@ K9s is available on Linux, macOS and Windows platforms. ```shell sudo port install k9s ``` + * Via [snap](https://snapcraft.io/k9s) for Linux ```shell @@ -81,6 +113,7 @@ K9s is available on Linux, macOS and Windows platforms. ``` * Via [Winget](https://github.com/microsoft/winget-cli) for Windows + ```shell winget install k9s ``` @@ -110,6 +143,12 @@ K9s is available on Linux, macOS and Windows platforms. curl -sS https://webinstall.dev/k9s | bash ``` +* Via [pkgx](https://pkgx.dev/pkgs/k9scli.io/) for Linux and macOS + + ```shell + pkgx k9s + ``` + * Via [Webi](https://webinstall.dev) for Windows ```shell @@ -126,7 +165,8 @@ K9s is available on Linux, macOS and Windows platforms. ## Building From Source - K9s is currently using go v1.14 or above. In order to build K9s from source you must: + K9s is currently using GO v1.21.X or above. + In order to build K9s from source you must: 1. Clone the repo 2. Build and run the executable @@ -158,7 +198,7 @@ K9s is available on Linux, macOS and Windows platforms. You can build your own Docker image of k9s from the [Dockerfile](Dockerfile) with the following: ```shell - docker build -t k9s-docker:0.1 . + docker build -t k9s-docker:v0.0.1 . ``` You can get the latest stable `kubectl` version and pass it to the `docker build` command with the `--build-arg` option. @@ -185,16 +225,14 @@ K9s is available on Linux, macOS and Windows platforms. export TERM=xterm-256color ``` -* In order to issue manifest edit commands make sure your EDITOR env is set. +* In order to issue resource edit commands make sure your EDITOR and KUBE_EDITOR env vars are set. ```shell # Kubectl edit command will use this env var. - export EDITOR=my_fav_editor - # Should your editor deal with streamed vs on disk files differently, also set... - export K9S_EDITOR=my_fav_editor + export KUBE_EDITOR=my_fav_editor ``` -* K9s prefers recent kubernetes versions ie 1.16+ +* K9s prefers recent kubernetes versions ie 1.28+ --- @@ -202,118 +240,134 @@ K9s is available on Linux, macOS and Windows platforms. | k9s | k8s client | | ------------------ | ---------- | -| >= v0.27.0 | 0.26.1 | -| v0.26.7 - v0.26.6 | 0.25.3 | -| v0.26.5 - v0.26.4 | 0.25.1 | -| v0.26.3 - v0.26.1 | 0.24.3 | -| v0.26.0 - v0.25.19 | 0.24.2 | -| v0.25.18 - v0.25.3 | 0.22.3 | -| v0.25.2 - v0.25.0 | 0.22.0 | -| <= v0.24 | 0.21.3 | +| >= v0.27.0 | 1.26.1 | +| v0.26.7 - v0.26.6 | 1.25.3 | +| v0.26.5 - v0.26.4 | 1.25.1 | +| v0.26.3 - v0.26.1 | 1.24.3 | +| v0.26.0 - v0.25.19 | 1.24.2 | +| v0.25.18 - v0.25.3 | 1.22.3 | +| v0.25.2 - v0.25.0 | 1.22.0 | +| <= v0.24 | 1.21.3 | --- ## The Command Line ```shell -# List all available CLI options -k9s help +# List current version +k9s version + # To get info about K9s runtime (logs, configs, etc..) k9s info + +# List all available CLI options +k9s help + # To run K9s in a given namespace k9s -n mycoolns + # Start K9s in an existing KubeConfig context k9s --context coolCtx + # Start K9s in readonly mode - with all cluster modification commands disabled k9s --readonly ``` -## Logs +## Logs And Debug Logs -Given the nature of the ui k9s does produce logs to a specific location. To view the logs and turn on debug mode, use the following commands: +Given the nature of the ui k9s does produce logs to a specific location. +To view the logs and turn on debug mode, use the following commands: ```shell +# Find out where the logs are stored k9s info -# Will produces something like this -# ____ __.________ -# | |/ _/ __ \______ -# | < \____ / ___/ -# | | \ / /\___ \ -# |____|__ \ /____//____ > -# \/ \/ -# -# Configuration: ~/Library/Preferences/k9s/config.yml -# Logs: /var/folders/8c/hh6rqbgs5nx_c_8k9_17ghfh0000gn/T/k9s-fernand.log -# Screen Dumps: /var/folders/8c/hh6rqbgs5nx_c_8k9_17ghfh0000gn/T/k9s-screens-fernand - -# To view k9s logs -tail -f /var/folders/8c/hh6rqbgs5nx_c_8k9_17ghfh0000gn/T/k9s-fernand.log - -# Start K9s in debug mode -k9s -l debug ``` -## Key Bindings +```text + ____ __.________ +| |/ _/ __ \______ +| < \____ / ___/ +| | \ / /\___ \ +|____|__ \ /____//____ > + \/ \/ + +Version: vX.Y.Z +Config: /Users/fernand/.config/k9s/config.yaml +Logs: /Users/fernand/.local/state/k9s/k9s.log +Dumps dir: /Users/fernand/.local/state/k9s/screen-dumps +Benchmarks dir: /Users/fernand/.local/state/k9s/benchmarks +Skins dir: /Users/fernand/.local/share/k9s/skins +Contexts dir: /Users/fernand/.local/share/k9s/clusters +Custom views file: /Users/fernand/.local/share/k9s/views.yaml +Plugins file: /Users/fernand/.local/share/k9s/plugins.yaml +Hotkeys file: /Users/fernand/.local/share/k9s/hotkeys.yaml +Alias file: /Users/fernand/.local/share/k9s/aliases.yaml +``` -K9s uses aliases to navigate most K8s resources. +### View K9s logs -| Action | Command | Comment | -|----------------------------------------------------------------|-------------------------------|------------------------------------------------------------------------| -| Show active keyboard mnemonics and help | `?` | | -| Show all available resource alias | `ctrl-a` | | -| To bail out of K9s | `:q`, `ctrl-c` | | -| View a Kubernetes resource using singular/plural or short-name | `:`po⏎ | accepts singular, plural, short-name or alias ie pod or pods | -| View a Kubernetes resource in a given namespace | `:`alias namespace⏎ | | -| Filter out a resource view given a filter | `/`filter⏎ | Regex2 supported ie `fred|blee` to filter resources named fred or blee | -| Inverse regex filter | `/`! filter⏎ | Keep everything that *doesn't* match. | -| Filter resource view by labels | `/`-l label-selector⏎ | | -| Fuzzy find a resource given a filter | `/`-f filter⏎ | | -| Bails out of view/command/filter mode | `` | | -| Key mapping to describe, view, edit, view logs,... | `d`,`v`, `e`, `l`,... | | -| To view and switch to another Kubernetes context (Pod view) | `:`ctx⏎ | | -| To view and switch directly to another Kubernetes context (Last used view) | `:`ctx context-name⏎ | | -| To view and switch to another Kubernetes namespace | `:`ns⏎ | | -| To view all saved resources | `:`screendump or sd⏎ | | -| To delete a resource (TAB and ENTER to confirm) | `ctrl-d` | | -| To kill a resource (no confirmation dialog, equivalent to kubectl delete --now) | `ctrl-k` | | -| Launch pulses view | `:`pulses or pu⏎ | | -| Launch XRay view | `:`xray RESOURCE [NAMESPACE]⏎ | RESOURCE can be one of po, svc, dp, rs, sts, ds, NAMESPACE is optional | -| Launch Popeye view | `:`popeye or pop⏎ | See [popeye](#popeye) | +```shell +tail -f /Users/fernand/.local/data/k9s/k9s.log +``` ---- +### Start K9s in debug mode -## Screenshots +```shell +k9s -l debug +``` -1. Pods - -1. Logs - -1. Deployments - +### Customize logs destination ---- +You can override the default log file destination either with the `--logFile` argument: ---- +```shell +k9s --logFile /tmp/k9s.log +less /tmp/k9s.log +``` -## Demo Videos/Recordings +Or through the `K9S_LOGS_DIR` environment variable: -* [k9s Kubernetes UI - A Terminal-Based Vim-Like Kubernetes Dashboard](https://youtu.be/boaW9odvRCc) -* [K9s v0.21.3](https://youtu.be/wG8KCwDAhnw) -* [K9s v0.19.X](https://youtu.be/kj-WverKZ24) -* [K9s v0.18.0](https://www.youtube.com/watch?v=zMnD5e53yRw) -* [K9s v0.17.0](https://www.youtube.com/watch?v=7S33CNLAofk&feature=youtu.be) -* [K9s Pulses](https://asciinema.org/a/UbXKPal6IWpTaVAjBBFmizcGN) -* [K9s v0.15.1](https://youtu.be/7Fx4XQ2ftpM) -* [K9s v0.13.0](https://www.youtube.com/watch?v=qaeR2iK7U0o&t=15s) -* [K9s v0.9.0](https://www.youtube.com/watch?v=bxKfqumjW4I) -* [K9s v0.7.0 Features](https://youtu.be/83jYehwlql8) -* [K9s v0 Demo](https://youtu.be/k7zseUhaXeU) +```shell +K9S_LOGS_DIR=/var/log k9s +less /var/log/k9s.log +``` + +## Key Bindings + +K9s uses aliases to navigate most K8s resources. + +| Action | Command | Comment | +|---------------------------------------------------------------------------------|-------------------------------|------------------------------------------------------------------------| +| Show active keyboard mnemonics and help | `?` | | +| Show all available resource alias | `ctrl-a` | | +| To bail out of K9s | `:quit`, `:q`, `ctrl-c` | | +| To go up/back to the previous view | `esc` | If you have crumbs on, this will go to the previous one | +| View a Kubernetes resource using singular/plural or short-name | `:`pod⏎ | accepts singular, plural, short-name or alias ie pod or pods | +| View a Kubernetes resource in a given namespace | `:`pod ns-x⏎ | | +| View filtered pods (New v0.30.0!) | `:`pod /fred⏎ | View all pods filtered by fred | +| View labeled pods (New v0.30.0!) | `:`pod app=fred,env=dev⏎ | View all pods with labels matching app=fred and env=dev | +| View pods in a given context (New v0.30.0!) | `:`pod @ctx1⏎ | View all pods in context ctx1. Switches out your current k9s context! | +| Filter out a resource view given a filter | `/`filter⏎ | Regex2 supported ie `fred|blee` to filter resources named fred or blee | +| Inverse regex filter | `/`! filter⏎ | Keep everything that *doesn't* match. | +| Filter resource view by labels | `/`-l label-selector⏎ | | +| Fuzzy find a resource given a filter | `/`-f filter⏎ | | +| Bails out of view/command/filter mode | `` | | +| Key mapping to describe, view, edit, view logs,... | `d`,`v`, `e`, `l`,... | | +| To view and switch to another Kubernetes context (Pod view) | `:`ctx⏎ | | +| To view and switch directly to another Kubernetes context (Last used view) | `:`ctx context-name⏎ | | +| To view and switch to another Kubernetes namespace | `:`ns⏎ | | +| To view all saved resources | `:`screendump or sd⏎ | | +| To delete a resource (TAB and ENTER to confirm) | `ctrl-d` | | +| To kill a resource (no confirmation dialog, equivalent to kubectl delete --now) | `ctrl-k` | | +| Launch pulses view | `:`pulses or pu⏎ | | +| Launch XRay view | `:`xray RESOURCE [NAMESPACE]⏎ | RESOURCE can be one of po, svc, dp, rs, sts, ds, NAMESPACE is optional | +| Launch Popeye view | `:`popeye or pop⏎ | See [popeye](#popeye) | --- ## K9s Configuration - K9s keeps its configurations 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 `K9SCONFIG` to tell K9s the directory location to pull its configurations from. + 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. | Unix | macOS | Windows | |-----------------|------------------------------------|-----------------------| @@ -321,96 +375,79 @@ K9s uses aliases to navigate most K8s resources. > NOTE: This is still in flux and will change while in pre-release stage! +You 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` + ```yaml - # $XDG_CONFIG_HOME/k9s/config.yml + # $XDG_CONFIG_HOME/k9s/config.yaml k9s: # Enable periodic refresh of resource browser windows. Default false liveViewAutoRefresh: false + # The path to screen dump. Default: '%temp_dir%/k9s-screens-%username%' (k9s info) + screenDumpDir: /tmp/dumps # Represents ui poll intervals. Default 2secs refreshRate: 2 # Number of retries once the connection to the api-server is lost. Default 15. maxConnRetry: 5 - # Enable mouse support. Default false - enableMouse: true - # Set to true to hide K9s header. Default false - headless: false - # Set to true to hide K9s crumbs. Default false - crumbsless: false # Indicates whether modification commands like delete/kill/edit are disabled. Default is false readOnly: false # Toggles whether k9s should exit when CTRL-C is pressed. When set to true, you will need to exist k9s via the :quit command. Default is false. noExitOnCtrlC: false + #UI settings + ui: + # Enable mouse support. Default false + enableMouse: false + # Set to true to hide K9s header. Default false + headless: false + # Set to true to hide the K9S logo Default false + logoless: false + # Set to true to hide K9s crumbs. Default false + crumbsless: false + noIcons: false + # Toggles reactive UI. This option provide for watching on disk artifacts changes and update the UI live Defaults to false. + reactive: false + # By default all contexts wil use the dracula skin unless explicitly overridden in the context config file. + skin: dracula # => assumes the file skins/dracula.yaml is present in the $XDG_DATA_HOME/k9s/skins directory + # Allows to set certain views default fullscreen mode. (yaml, helm history, describe, value_extender, details, logs) Default false + defaultsToFullScreen: false # Toggles icons display as not all terminal support these chars. noIcons: false # Toggles whether k9s should check for the latest revision from the Github repository releases. Default is false. skipLatestRevCheck: false + # 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. + keepMissingClusters: false # Logs configuration logger: # Defines the number of lines to return. Default 100 tail: 200 # Defines the total number of log lines to allow in the view. Default 1000 buffer: 500 - # Represents how far to go back in the log timeline in seconds. Setting to -1 will show all available logs. Default is 5min. - sinceSeconds: 300 - # Go full screen while displaying logs. Default false - fullScreenLogs: false + # Represents how far to go back in the log timeline in seconds. Setting to -1 will tail logs. Default is -1. + sinceSeconds: 300 # => tail the last 5 mins. # Toggles log line wrap. Default false textWrap: false # Toggles log line timestamp info. Default false showTime: false # Toggles whether JSON log lines should be colorized. Default false showJSON: false - # Indicates the current kube context. Defaults to current context - currentContext: minikube - # Indicates the current kube cluster. Defaults to current context cluster - currentCluster: minikube - # KeepMissingClusters will keep clusters in the config if they are missing from the current kubeconfig file. Default false - KeepMissingClusters: false - # Persists per cluster preferences for favorite namespaces and view. - clusters: - coolio: - namespace: - active: coolio - # With this set, the favorites list won't be updated as you switch namespaces - lockFavorites: false - favorites: - - cassandra - - default - view: - active: po - featureGates: - # Toggles NodeShell support. Allow K9s to shell into nodes if needed. Default false. - nodeShell: false - # Provide shell pod customization of feature gate is enabled - shellPod: - # The shell pod image to use. - image: killerAdmin - # The namespace to launch to shell pod into. - namespace: fred - # The resource limit to set on the shell pod. - limits: - cpu: 100m - memory: 100Mi - # The IP Address to use when launching a port-forward. - portForwardAddress: 1.2.3.4 - kind: - namespace: - active: all - favorites: - - all - - kube-system - - default - view: - active: dp - # The path to screen dump. Default: '%temp_dir%/k9s-screens-%username%' (k9s info) - screenDumpDir: /tmp + # Provide shell pod customization when nodeShell feature gate is enabled! + shellPod: + # The shell pod image to use. + image: killerAdmin + # The namespace to launch to shell pod into. + namespace: default + # The resource limit to set on the shell pod. + limits: + cpu: 100m + memory: 100Mi + # Enable TTY + tty: true ``` --- ## Popeye Configuration -K9s 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/k9s/_spinach.yml`. This allows you to have a different spinach config per cluster. +K9s 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. --- @@ -418,51 +455,75 @@ K9s has integration with [Popeye](https://popeyecli.io/), which is a Kubernetes By 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: +Alternatively, 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` + +```yaml +# $XDG_CONFIG_HOME/k9s/config.yaml +k9s: + # You can also further tune the shell pod specification + shellPod: + image: cool_kid_admin:42 + namespace: blee + limits: + cpu: 100m + memory: 100Mi +``` + +Then in your cluster configuration file... + ```yaml -# $XDG_CONFIG_HOME/k9s/config.yml +# $XDG_DATA_HOME/k9s/clusters/cluster-1/context-1 k9s: - clusters: - # Configures node shell on cluster blee - blee: - featureGates: - # You must enable the nodeShell feature gate to enable shelling into nodes - nodeShell: true - # You can also further tune the shell pod specification - shellPod: - image: cool_kid_admin:42 - namespace: blee - limits: - cpu: 100m - memory: 100Mi + cluster: cluster-1 + readOnly: false + namespace: + active: default + lockFavorites: false + favorites: + - kube-system + - default + view: + active: po + featureGates: + nodeShell: true # => Enable this feature gate to make nodeShell available on this cluster + portForwardAddress: localhost ``` --- ## Command Aliases -In K9s, you can define your very own command aliases (shortnames) to access your resources. In your `$HOME/.config/k9s` define a file called `alias.yml`. A 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: +In 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`. +A 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: ```yaml -# $XDG_CONFIG_HOME/k9s/alias.yml -alias: +# $XDG_DATA_HOME/k9s/aliases.yaml +aliases: pp: v1/pods crb: rbac.authorization.k8s.io/v1/clusterrolebindings + # As of v0.30.0 you can also refer to another command alias... + fred: pod fred app=blee # => view pods in namespace fred with labels matching app=blee ``` -Using this alias file, you can now type pp/crb to list pods or ClusterRoleBindings respectively. +Using this aliases file, you can now type `:pp` or `:crb` or `:fred` to activate their respective commands. --- ## HotKey Support -Entering the command mode and typing a resource name or alias, could be cumbersome for navigating thru often used resources. We're introducing hotkeys that allows a user to define their own hotkeys to activate their favorite resource views. In order to enable hotkeys please follow these steps: +Entering the command mode and typing a resource name or alias, could be cumbersome for navigating thru often used resources. +We're introducing hotkeys that allow users to define their own key combination to activate their favorite resource views. -1. Create a file named `$XDG_CONFIG_HOME/k9s/hotkey.yml` -2. Add the following to your `hotkey.yml`. You can use resource name/short name to specify a command ie same as typing it while in command mode. +Additionally, you can define context specific hotkeys by add a context level configuration file in `$XDG_DATA_HOME/k9s/clusters/clusterX/contextY/hotkeys.yaml` + +In order to surface hotkeys globally please follow these steps: + +1. Create a file named `$XDG_CONFIG_HOME/k9s/hotkeys.yaml` +2. 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. ```yaml - # $XDG_CONFIG_HOME/k9s/hotkey.yml - hotKey: + # $XDG_CONFIG_HOME/k9s/hotkeys.yaml + hotKeys: # Hitting Shift-0 navigates to your pod view shift-0: shortCut: Shift-0 @@ -478,12 +539,22 @@ Entering the command mode and typing a resource name or alias, could be cumberso shortCut: Shift-2 description: Xray Deployments command: xray deploy + # Hitting Shift-S view the resources in the namespace of your current selection + shift-s: + shortCut: Shift-S + override: true # => will override the default shortcut related action if set to true (default to false) + description: Namespaced resources + command: "$RESOURCE_NAME $NAMESPACE" + keepHistory: true # whether you can return to the previous view ``` - Not feeling so hot? Your custom hotkeys will be listed in the help view `?`. Also your hotkey file will be automatically reloaded so you can readily use your hotkeys as you define them. + Not feeling so hot? Your custom hotkeys will be listed in the help view `?`. + Also your hotkeys file will be automatically reloaded so you can readily use your hotkeys as you define them. You can choose any keyboard shortcuts that make sense to you, provided they are not part of the standard K9s shortcuts list. + Similarly, referencing environment variables in hotkeys is also supported. The available environment variables can refer to the description in the [Plugins](#plugins) section. + > NOTE: This feature/configuration might change in future releases! --- @@ -492,9 +563,9 @@ Entering the command mode and typing a resource name or alias, could be cumberso As 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: -- `k9scli.io/auto-port-forwards` +@ `k9scli.io/auto-port-forwards` activates one or more port-forwards directly bypassing the port-forward dialog all together. -- `k9scli.io/port-forwards` +@ `k9scli.io/port-forwards` pre-selects one or more port-forwards when launching the port-forward dialog. The annotation value takes on the shape `container-name::[local-port:]container-port` @@ -543,47 +614,50 @@ The annotation value must specify a container to forward to as well as a local p [SneakCast v0.17.0 on The Beach! - Yup! sound is sucking but what a setting!](https://youtu.be/7S33CNLAofk) -You 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.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! +You 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! > NOTE: This is experimental and will most likely change as we iron this out! Here is a sample views configuration that customize a pods and services views. ```yaml -# $XDG_CONFIG_HOME/k9s/views.yml -k9s: - views: - v1/pods: - columns: - - AGE - - NAMESPACE - - NAME - - IP - - NODE - - STATUS - - READY - v1/services: - columns: - - AGE - - NAMESPACE - - NAME - - TYPE - - CLUSTER-IP +# $XDG_CONFIG_HOME/k9s/views.yaml +views: + v1/pods: + columns: + - AGE + - NAMESPACE + - NAME + - IP + - NODE + - STATUS + - READY + v1/services: + columns: + - AGE + - NAMESPACE + - NAME + - TYPE + - CLUSTER-IP ``` --- ## Plugins -K9s allows you to extend your command line and tooling by defining your very own cluster commands via plugins. K9s will look at `$XDG_CONFIG_HOME/k9s/plugin.yml` to locate all available plugins. A plugin is defined as follows: +K9s allows you to extend your command line and tooling by defining your very own cluster commands via plugins. K9s will look at `$XDG_CONFIG_HOME/k9s/plugins.yaml` to locate all available plugins. + +A plugin is defined as follows: * Shortcut option represents the key combination a user would type to activate the plugin +* Override option make that the default action related to the shortcut will be overrided by the plugin * 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 * Description will be printed next to the shortcut in the k9s menu * 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. * Command represents ad-hoc commands the plugin runs upon activation * Background specifies whether or not the command runs in the background * Args specifies the various arguments that should apply to the command above +* OverwriteOutput options allows plugin developers to provide custom messages on plugin execution K9s does provide additional environment variables for you to customize your plugins arguments. Currently, the available environment variables are as follows: @@ -604,16 +678,17 @@ K9s does provide additional environment variables for you to customize your plug Curly 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}`) -### Example +### Plugin Example -This defines a plugin for viewing logs on a selected pod using `ctrl-l` for shortcut. +This defines a plugin for viewing logs on a selected pod using `ctrl-l` as shortcut. ```yaml -# $XDG_CONFIG_HOME/k9s/plugin.yml -plugin: +# $XDG_DATA_HOME/k9s/plugins.yaml +plugins: # Defines a plugin to provide a `ctrl-l` shortcut to tail the logs while in pod view. fred: shortCut: Ctrl-L + override: false confirm: false description: Pod logs scopes: @@ -647,12 +722,14 @@ Initially, the benchmarks will run with the following defaults: * HTTP Verb: GET * Path: / -The PortForward view is backed by a new K9s config file namely: `$XDG_CONFIG_HOME/k9s/bench-.yml` (note: extension is `yml` and not `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. +The 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. -Here is a sample benchmarks.yml configuration. Please keep in mind this file will likely change in subsequent releases! +Benchmarks result reports are stored in `$XDG_STATE_HOME/k9s/clusters/clusterX/contextY` + +Here is a sample benchmarks.yaml configuration. Please keep in mind this file will likely change in subsequent releases! ```yaml -# This file resides in $XDG_CONFIG_HOME/k9s/bench-mycontext.yml +# This file resides in $XDG_DATA_HOME/k9s/clusters/clusterX/contextY/benchmarks.yaml benchmarks: # Indicates the default concurrency and number of requests setting if a container or service rule does not match. defaults: @@ -800,21 +877,93 @@ Example: Dracula Skin ;) Dracula Skin -You 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. K9s skins are loaded from `$XDG_CONFIG_HOME/k9s/skin.yml`. If a skin file is detected then the skin would be loaded if not the current stock skin remains in effect. +You 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. +You 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. +In 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 +`skins/dracula.yaml` to `$XDG_CONFIG_HOME/k9s/skins/` directory. -You can also change K9s skins based on the cluster you are connecting too. In this case, you can specify the skin file name as `$XDG_CONFIG_HOME/k9s/mycontext_skin.yml` -Below is a sample skin file, more skins are available in the skins directory in this repo, just simply copy any of these in your user's home dir as `skin.yml`. +In the case where your cluster spans several contexts, you can add a skin context configuration to your context configuration. +This is a collection of {context_name, skin} tuples (please see example below!) Colors 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. > NOTE: This is very much an experimental feature at this time, more will be added/modified if this feature has legs so thread accordingly! +> NOTE: Please see [K9s Skins](https://k9scli.io/topics/skins/) for a list of available colors. +To skin a specific context and provided the file `in_the_navy.yaml` is present in your skins directory. -> NOTE: Please see [K9s Skins](https://k9scli.io/topics/skins/) for a list of available colors. +```yaml +# $XDG_DATA_HOME/k9s/clusters/clusterX/contextY/config.yaml +k9s: + cluster: clusterX + skin: in_the_navy + readOnly: false + namespace: + active: default + lockFavorites: false + favorites: + - kube-system + - default + view: + active: po + featureGates: + nodeShell: false + portForwardAddress: localhost +``` + +You can also specify a default skin for all contexts in the root k9s config file as so: +```yaml +# $XDG_CONFIG_HOME/k9s/config.yaml +k9s: + liveViewAutoRefresh: false + screenDumpDir: /tmp/dumps + refreshRate: 2 + maxConnRetry: 5 + readOnly: false + noExitOnCtrlC: false + ui: + enableMouse: false + headless: false + logoless: false + crumbsless: false + noIcons: false + # Toggles reactive UI. This option provide for watching on disk artifacts changes and update the UI live Defaults to false. + reactive: false + # By default all contexts wil use the dracula skin unless explicitly overridden in the context config file. + skin: dracula # => assumes the file skins/dracula.yaml is present in the $XDG_DATA_HOME/k9s/skins directory + defaultsToFullScreen: false + skipLatestRevCheck: false + disablePodCounting: false + shellPod: + image: busybox + namespace: default + limits: + cpu: 100m + memory: 100Mi + imageScans: + enable: false + exclusions: + namespaces: [] + labels: {} + logger: + tail: 100 + buffer: 5000 + sinceSeconds: -1 + textWrap: false + showTime: false + thresholds: + cpu: + critical: 90 + warn: 70 + memory: + critical: 90 + warn: 70 +``` ```yaml -# Skin InTheNavy... +# $XDG_DATA_HOME/k9s/skins/in_the_navy.yaml +# Skin InTheNavy! k9s: # General K9s styles body: @@ -910,7 +1059,7 @@ that you want, please file an issue and if so inclined submit a PR! K9s will most likely blow up if... -1. You're running older versions of Kubernetes. K9s works best on Kubernetes latest. +1. You're running older versions of Kubernetes. K9s works best on later Kubernetes versions. 2. You don't have enough RBAC fu to manage your cluster. --- @@ -941,4 +1090,4 @@ We always enjoy hearing from folks who benefit from our work! --- -Imhotep  © 2021 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) +Imhotep  © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.29.0.md b/change_logs/release_v0.29.0.md new file mode 100644 index 0000000000..927d84a794 --- /dev/null +++ b/change_logs/release_v0.29.0.md @@ -0,0 +1,212 @@ + + +# Release v0.29.0 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +--- + +## ♫ Sounds Behind The Release ♭ + +* [Snowbound - Donald Fagen](https://www.youtube.com/watch?v=bj8ZdBdKsfo) +* [Pilgrim - Eric Clapton](https://www.youtube.com/watch?v=8V9tSQuIzbQ) +* [Lucky Number - Lene Lovich](https://www.youtube.com/watch?v=KnIJOO__jVo) + +--- + +## 🦃 Happy (Belated!) ThanksGiving To All! 🦃 + +Hope you and yours had a wonderful holiday!! +Hopefully this drop won't be a cold turkey 😳 + +I'd like to take this opportunity to honor two very special folks: + +* [Alexandru Placinta](https://github.com/placintaalexandru) +* [Jayson Wang](https://github.com/wjiec) + +These guys have been relentless in fishing out bugs, helping out with support and addressing issues, not to mention enduring my code! 🙀 +They dedicate a lot of their time to make `k9s` better for all of us! +So if you happen to run into them live/virtual, please be sure to `Thank` them and give them a huge hug! 🤗 + +I am thankful for all of you for being kind, patient, understanding and one of the coolest OSS community on the web!! + +Feeling blessed and ever so humbled to be part of it. + +Thank you!! + +--- + +## A Word From Our Sponsors... + +To all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!! + +* [Marco Stuurman](https://github.com/fe-ax) +* [Paul Sweeney](https://github.com/Kolossi) +* [Cayla Fauver](https://github.com/cayla) +* [alemanek](https://github.com/alemanek) +* [Danske Commodities A/S](https://github.com/DanskeCommodities) + +> Sponsorship cancellations since the last release: **8** ;( + +--- + +## 🎉 Feature Release 🎈👯 + +--- + +### Breaking Bad! + +WARNING! There are breaking change on this drop! + +1. NodeShell configuration has moved up in the k9s config file from the context section to the top level config. +More 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. +Please see README for the details. + + > NOTE: If you haven't customize the shellPod images on your contexts, the app will move the nodeShell config section to + > it's new location and update your clusters information accordingly. + > If not, you will need to edit the nodeShell section and manage it from a single location! + +1. Log view used to default to the last 5mins aka `sinceSeconds: 300`. + Changed the default to tail logs instead aka `sinceSeconds: -1` + +1. 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. +Specify the name of the skin in the config file and now your cluster will load the specified skin. + +For 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: + +```yaml +k9s: + ... + clusters: + fred: + # Override the default skin and use this skin for this cluster. + skin: one_dark # -> Look for a skin file in ~/.config/k9s/skins/one_dark.yml + namespace: + ... + view: + active: pod + featureGates: + nodeShell: false + portForwardAddress: localhost +``` + +The `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. + +--- + +### Walk Of SHelm... + +Added a `Releases` view to Helm! + +This provides the ability for Helm users to manage their releases directly from k9s. +You can now press `enter` on a selected Helm install and view all associated releases. +While in the releases view, you can also rollback an install to a previous revision. + +--- + +### Spock! Are You Out Of Your VulScan Mind? + +Tired of having malignent folks shoot holes in your prod clusters or failing compliance testing? + +Added ability to run image vulnerability scans directly from k9s. You can now monitor your security stance in dev/staging/... clusters +prior to proclaiming `It's Open Season...` in prod! + +As it stands Pod, Deployment, StatefulSet, DaemonSet, CronJob, Job views will feature a new column for Vulnerability Scan aka `VS`. + +> NOTE! This feature is gated so you'll need to manually opt in/out by modifying your k9s config file like so: + +```yaml +k9s: + liveViewAutoRefresh: false + enableImageScan: true # <- Yes Please!! + headless: false + ... +``` + +Once 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!!*). +The `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 +For 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. + +The 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... +Once the caches are primed, subsequent loads should be faster 🤞 + +You can sort the views by vulnerability score using `ShiftV`. +Additionally, you can view the full scans report by pressing `v` on a selected resource. + +I've synced my entire Thanksgiving holiday break on this ding dang deal, so hopefully it works for most of you?? +Also if you dig this new feature, please make some noise! 😍 + +💘 This is an experimental feature and likely will require additional TLC 💘 + +> NOTE! The lib we use to scan for vulnerabilities only supports macOS and Linux!! +> NOTE: I have yet to test this feature on larger clusters, so likely this may break?? +> Please take these reports with a grain of salt as likely your mileage will vary and help us +> validate the accuracy of the report ie if we cry `Wolf`, is it actually there? + +The paint is still fresh on this deal!! + +### Do You Tube? + +My plan is to begin (again!) putting out short k9s episodes with how-tos, tips, tricks and features previews. + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +The first drop should be up by the time you read this! + +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2308](https://github.com/derailed/k9s/issues/2308) Unable to list CRs for crd with only list and get verb without watch verb +* [#2301](https://github.com/derailed/k9s/issues/2301) Add imagePullPolicy and imagePullSecrets on shell_pod for internal registry uses +* [#2298](https://github.com/derailed/k9s/issues/2298) Weird color after plugin usage +* [#2297](https://github.com/derailed/k9s/issues/2297) Select nodes with space does not work anymore +* [#2290](https://github.com/derailed/k9s/issues/2290) Provide release assets for freebsd amd64/arm64 +* [#2283](https://github.com/derailed/k9s/issues/2283) Adding auto complete in search bar +* [#2219](https://github.com/derailed/k9s/issues/2219) Add tty: true to the node shell pod manifest +* [#2167](https://github.com/derailed/k9s/issues/2167) Show wrong Configmap data +* [#2166](https://github.com/derailed/k9s/issues/2166) Taint count for the nodes view +* [#2165](https://github.com/derailed/k9s/issues/2165) Restart counter for init containers +* [#2162](https://github.com/derailed/k9s/issues/2162) Make edit work when describing a resource +* [#2154](https://github.com/derailed/k9s/issues/2154) Help and h command does not work if typed into cmdbuff +* [#2036](https://github.com/derailed/k9s/issues/2036) Crashed while do filtering +* [#2009](https://github.com/derailed/k9s/issues/2009) Ctrl-s: Name of file (Describe-....) +* [#1513](https://github.com/derailed/k9s/issues/1513) Problem regarding showing the logs - it hangs/slow on pods which are running for long time + NOTE: Better but not cured! Perf improvements while viewing large cm (7k lines) from 26s->9s +* [#568](https://github.com/derailed/k9s/issues/568) Allow both .yaml and .yml yaml config files + +--- + +## Contributed PRs + +Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!! + +* [#2322](https://github.com/derailed/k9s/pull/2322) Check if the service provides selectors +* [#2319](https://github.com/derailed/k9s/pull/2319) Proper handling of help commands (fixes #2154) +* [#2315](https://github.com/derailed/k9s/pull/2315) Fix namespace suggestion error on context switch +* [#2313](https://github.com/derailed/k9s/pull/2313) Should not clear screen when executing plugin command +* [#2310](https://github.com/derailed/k9s/pull/2310) chore: Mot recommended to use k8s.io/kubernetes as a dependency +* [#2303](https://github.com/derailed/k9s/pull/2303) Clean up items +* [#2301](https://github.com/derailed/k9s/pull/2301) feat: Add imagePullSecrets and imagePullPolicy configuration for shellpod +* [#2289](https://github.com/derailed/k9s/pull/2289) Clean up issues introduced in #2125 +* [#2288](https://github.com/derailed/k9s/pull/2288) Fix merge issues from PR #2168 +* [#2284](https://github.com/derailed/k9s/issues/2284) Allow both .yaml and .yml yaml config files + +--- + + © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.29.1.md b/change_logs/release_v0.29.1.md new file mode 100644 index 0000000000..2c58c8f944 --- /dev/null +++ b/change_logs/release_v0.29.1.md @@ -0,0 +1,34 @@ + + +# Release v0.29.1 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +--- + +## Maintenance Release + +--- + +## Resolved Issues + +* [#2330](https://github.com/derailed/k9s/issues/2330) Skins don't work v0.29.0 +* [#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 +* [#2327](https://github.com/derailed/k9s/issues/2327) [Bug] Item highlighting broke in v0.29.0 + +--- + + © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.30.0.md b/change_logs/release_v0.30.0.md new file mode 100644 index 0000000000..54d31df2c8 --- /dev/null +++ b/change_logs/release_v0.30.0.md @@ -0,0 +1,313 @@ + + +# Release v0.30.0 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +--- + +## ♫ Sounds Behind The Release ♭ + +Going back to the classics... + +* [Home For Christmas - Fats Domino](https://www.youtube.com/watch?v=ykAVdPz8o1Q) +* [Our Love - Al Jarreau](https://www.youtube.com/watch?v=9ztMe6GIwi8) +* [Body And Soul - Louis Armstrong](https://www.youtube.com/watch?v=2Gnz69TbqHQ) +* [On The Dunes - Donald Fagen](https://www.youtube.com/watch?v=QoVT3XcMVvk) +* [Ciao - Lucio Dalla](https://www.youtube.com/watch?v=qcqXcmKu_I4) +* [Basin Street Blues - Louis Prima](https://www.youtube.com/watch?v=IijXXXpUefM&list=RDIijXXXpUefM&start_radio=1) + +--- + +## A Word From Our Sponsors... + +To all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!! + +* [Bojan](https://github.com/rbojan) + +> Sponsorship cancellations since the last release: **5!** 🥹 + +--- + +## 🎄 Feature Release! 🎄 + +🎅 Merry Christmas to all and Best wishes for the new year!!🧑‍🎄 + +--- + +### Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +### Breaking Bad! + +> ☢️ !!Prior to installing v0.30.0!! Please be sure to backup your k9s configs directories or move them somewhere safe!! + +> ☢️ Please watch the v0.30.0 Sneak peek series (links below) for detailed information. +> +> ☢️ Most K9s configuration files have either split or changed location or names on this drop!! + +> 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 +> to their new spec and location. You can then use your existing setup and patch with the new layout/spec. +> 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. +> If you find doc issues either file an issue or better yet submit a PR! + +Some of you might say: `You're on the roll their bud! Two breaking changes drops in a row!!` +Per the wise words of my beloved Grand mama! `One can't cook a decent meal without creating a mess!` +Not to mention we're still at v0.x.y so `Open season on breaking changes` is very much in full effect. + +Tho I have tested this drop quite a bit, there is a strong chance that I've broken some stuff. +The key here is to walk the fine line of improving k9s code base and features set with minimal impact to you. +As you know by now, I am committed to ease the pain and resolve issues quickly to get you all back up and running. + +From the scope changes in this release, I would caution that this drop will likely break you! +If so, worry not! We will fix the duds so we are `Happy as a Hippo` once again. + +There was a few issues with the way K9s persists it's configuration and various artifacts. So we rewrote it! +First and foremost all k9s related YAML resources, will now use the standard ".yaml" extension. +I think we've bloated the code checking for both extensions with no real actionable value! + +As 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 a your preferred k9s configs location. Also all config files will now use the ".yaml" extension vs ".yml"!! + +So the main k9s configuration (static) now looks like this: + +```yaml +# $XDG_CONFIG_HOME/k9s/config.yaml +# File will be autogenerated will all the default fixins if not found in the config specification. +k9s: + liveViewAutoRefresh: false + refreshRate: 2 + maxConnRetry: 5 + readOnly: false + noExitOnCtrlC: false + ui: # NOTE! New level!! + enableMouse: false + headless: false + logoless: false + crumbsless: false + noIcons: false + skipLatestRevCheck: false + disablePodCounting: false + # ShellPod configuration applies to all your clusters + shellPod: + image: busybox:1.35.0 + namespace: default + limits: + cpu: 100m + memory: 100Mi + # ImageScan config changed from v0.29.0! + imageScans: + enable: false + # Now figures exclusions ie excludes certain namespaces or specific workload labels + exclusions: + # Exclude the following namespaces for image vulscans! + namespaces: + - kube-system + - fred + # Exclude the following labels from image vulscans! + labels: + k8s-app: + - kindnet + - bozo + env: + - dev + logger: + tail: 100 + buffer: 5000 + sinceSeconds: -1 + fullScreenLogs: false + textWrap: false + showTime: false + thresholds: + cpu: + critical: 90 + warn: 70 + memory: + critical: 90 + warn: 70 +``` + +Next context specific configurations that are managed by you and k9s live in the XDG data directory +i.e `$XDG_DATA_HOME/k9s/clusters` or `$K9S_CONFIG_DIR/clusters` if the env var is set. + +```text +$XDG_DATA_HOME/k9s +// Clusters tracks visited kubeconfig cluster/contexts +├── clusters +│ ├── fred +│ │ └── bozo +│ │ └── config.yaml +│ ├── bozorg +│ │ ├── kind-bozo-1 +│ │ │ └── config.yaml +│ │ ├── kind-bozo-2 +│ │ │ └── config.yaml +│ │ └── kind-bozo-3 +│ │ └── config.yaml +│ └── bumblebeetuna +│ └── blee +│ └── config.yaml +└── skins + ├── black_and_wtf.yaml + ├── dracula.yaml + ├── in_the_navy.yml + ├── ... +``` + +Now looking at a given context configuration i.e cluster-1/context-1/config.yaml + +```yaml +# $XDG_DATA_HOME/k9s/clusters/bumblebeetuna/blee/config.yaml +k9s: + cluster: bumblebeetuna + readOnly: false # [New!] you can now single out a given context and make it readonly. Woof! + skin: in_the_navy # [NEW!] you can also skin individual contexts. Woof Woof! + namespace: + active: all + lockFavorites: false + favorites: + - all + - kube-system + - default + view: + active: dp + featureGates: + nodeShell: false + portForwardAddress: localhost +``` + +Transient artifacts ie k9s logs, screen-dumps, benchmarks etc now live in the state config dir. + +```text +$XDG_STATE_HOME/k9s +├── k9s.log # K9s log files +└── screen-dumps + └── bumblebeetuna # Screen dumps location for context blee + └── blee + └── deployments-kube-system-1703018199222861000.csv +``` + +If you get stuck or if my instructions are just `clear as mud`... `k9s info` is always your friend!! + +I feel this is an improvement (tho I might be unanimous on this!) especially for folks dealing with multi-clusters or swapping out there kubeconfigs... + +> NOTE! Paint is still fresh on this deal. Proceed with caution and please help us flush this feature out! + +--- + +# Got Prompt? + +In this drop, we've also gave the k9s command prompt aka `:xxx` some love. +You have the ability to specify filter directly in the prompt. + +So 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. +And now for the`Krampus` special... you can see pods in a different context all together via `:pod @ctx-2`. +Finally you can combo and send the `whole enchilada` via `:po k8s-app=fred /blee ns-1 @ctx-x` +Did I mention with completion where applicable? Yes Please!! +Compliments of [Jayson Wang](https://github.com/wjiec). Be sure to thank him!! + +Put these frequent flyers command in an alias and now you can nav your clusters with `even more style`! + +--- + +# All Is Love? + +🎵 `On The twentieth day of Christmas my true love gave to me... Ten worklords a-leaping??...` 🎵 + +This 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 `kubetcl 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. + +--- + +# Vulnerability Scan Exclusions... + +As it seems customary with all k9s new features, folks want to turn them off ;( +The `Vulscan` feature did not get out unscaped ;( +As it was rightfully so pointed out, you may want to opted out scans for images that you do not control. +Tho I think it might be a good idea to run wide open once in a while to see if your cluster has any holes?? +For this reason, we've opted to intro an exclusion section under the image scan configuration to exclude certain images from the scans. + +Here is a sample configuration: + +```yaml +k9s: + liveViewAutoRefresh: false + refreshRate: 2 + ui: + enableMouse: false + headless: false + logoless: false + crumbsless: false + noIcons: false + imageScans: + enable: true + exclusions: + # Skip scans on these namespaces + namespaces: + - ns-1 + - ns-2 + # Skip scans for pods matching these labels + labels: + - app: + - fred + - blee + - duh + - env: + - dev +``` + +This 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 ;( + +🎵 `On The second day of Christmas my true love gave to me... Eleven buggers bugging??...` 🎵 + +Lastly 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!` + +--- + +🎅 Best wishes to you and yours for good health and happiness this holiday season!! 🎉 + +AndJoy! +Fernand + +--- + +## Resolved Issues + +* [#2346](https://github.com/derailed/k9s/issues/2346) k9s should not write state to config.yaml +* [#2335](https://github.com/derailed/k9s/issues/2335) Restore 0.28 column order on pod view bug +* [#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. +* [#2283](https://github.com/derailed/k9s/issues/2283) Adding auto complete in search bar + +--- + +## Contributed PRs + +Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!! + +* [#2357](https://github.com/derailed/k9s/pull/2357) Added ln check for snap +* [#2350](https://github.com/derailed/k9s/pull/2350) Add symlink into snap +* [#2348](https://github.com/derailed/k9s/pull/2348) Fix(misc plugins): split up multiline commands, use less -K everywhere +* [#2343](https://github.com/derailed/k9s/pull/2343) Passing on the correct suggestion parameters +* [#2341](https://github.com/derailed/k9s/pull/2340) Adding value, yaml and describe views to helm-history +* [#2340](https://github.com/derailed/k9s/pull/2340) Add pkgx to installation section + +--- + + © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.30.1.md b/change_logs/release_v0.30.1.md new file mode 100644 index 0000000000..362929be30 --- /dev/null +++ b/change_logs/release_v0.30.1.md @@ -0,0 +1,57 @@ + + +# Release v0.30.1 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## 🎄 Maintenance Release! 🎄 + +🎵 `On The eleventh day of Christmas my true love gave to me... Bugs!!` 🎵 + +Got to love the aftermath... Thank you all for pitch'in in and help flesh out bugs!! The gift that keeps on... giving? + +🎅 Merry Christmas to all and Best wishes for the new year!!🧑‍🎄 + +--- + +### Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2368](https://github.com/derailed/k9s/issues/2368) Pod CPU and MEM columns are empty in 0.30.0 +* [#2367](https://github.com/derailed/k9s/issues/2367) k9s 0.30.0 issue loading plugins +* [#2366](https://github.com/derailed/k9s/issues/2366) List pods of deployment is now impossible +* [#2364](https://github.com/derailed/k9s/issues/2364) k9s 0.30.0 fields and values missed in action in the "namespace view" +* [#2363](https://github.com/derailed/k9s/issues/2363) Default 0.30.0 default skin on macOS is no good + +--- + +## Contributed PRs + +Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!! + +* [#2360](https://github.com/derailed/k9s/pull/2360) adding cancelable launch prompts to NodeShell + +--- + + © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.30.2.md b/change_logs/release_v0.30.2.md new file mode 100644 index 0000000000..1ab582e73a --- /dev/null +++ b/change_logs/release_v0.30.2.md @@ -0,0 +1,96 @@ + + +# Release v0.30.2 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## 🎄 Maintenance Release! 🎄 + +🎵 `On The eleventh day of Christmas my true love gave to me... More Bugs!!` 🎵 + +Thank you all for pitching in and help flesh out bugs!! + +--- + +## [!!FEATURE NAME CHANGED!!] Vulnerability Scan Exclusions... + +As it seems customary with all k9s new features, folks want to turn them off ;( +The `Vulscan` feature did not get out unscaped ;( +As it was rightfully so pointed out, you may want to opted out scans for images that you do not control. +Tho I think it might be a good idea to run wide open once in a while to see if your cluster has any holes?? +For this reason, we've opted to intro an exclusion section under the image scan configuration to exclude certain images from the scans. + +Here is a sample configuration: + +```yaml +k9s: + liveViewAutoRefresh: false + refreshRate: 2 + ui: + enableMouse: false + headless: false + logoless: false + crumbsless: false + noIcons: false + imageScans: + enable: true + # MOTE!! Field Name changed!! + exclusions: + # Skip scans on these namespaces + namespaces: + - ns-1 + - ns-2 + # Skip scans for pods matching these labels + labels: + - app: + - fred + - blee + - duh + - env: + - dev +``` + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2374](https://github.com/derailed/k9s/issues/2374) The headless parameter does not function properly (v0.30.1) +* [#2372](https://github.com/derailed/k9s/issues/2372) Unable to set default resource to load (v0.30.1) +* [#2371](https://github.com/derailed/k9s/issues/2371) --write cli option does not work (0.30.X) +* [#2370](https://github.com/derailed/k9s/issues/2370) Wrong list of pods on node (0.30.X) +* [#2362](https://github.com/derailed/k9s/issues/2362) blackList: Use inclusive language alternatives + +--- + +## Contributed PRs + +Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!! + +* [#2375](https://github.com/derailed/k9s/pull/2375) get node filtering params from matching context values +* [#2373](https://github.com/derailed/k9s/pull/2373) fix command line flags not working + +--- + + © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.30.3.md b/change_logs/release_v0.30.3.md new file mode 100644 index 0000000000..f40bcd95b6 --- /dev/null +++ b/change_logs/release_v0.30.3.md @@ -0,0 +1,45 @@ + + +# Release v0.30.3 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## 🎄 Maintenance Release! 🎄 + +🎵 `On The twelfth day of Christmas my true love gave to me... More Bugs!!` 🎵 + +Thank you all for pitching in and help flesh out issues!! + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2379](https://github.com/derailed/k9s/issues/2379) Filtering with equal sign (=) does not work in 0.30.X +* [#2378](https://github.com/derailed/k9s/issues/2378) Logs directory not created in the k9s config/home dir 0.30.1 +* [#2377](https://github.com/derailed/k9s/issues/2377) Opening AWS EKS contexts create two directories per cluster 0.30.1 + +--- + + © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.30.4.md b/change_logs/release_v0.30.4.md new file mode 100644 index 0000000000..c7a1ad2063 --- /dev/null +++ b/change_logs/release_v0.30.4.md @@ -0,0 +1,52 @@ + + +# Release v0.30.4 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## 🎄 Maintenance Release! 🎄 + +Thank you all for pitching in and helping flesh out issues!! + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2391](https://github.com/derailed/k9s/issues/2391) Version 0.30.* has issues with : chars in the cluster names from AWS +* [#2397](https://github.com/derailed/k9s/issues/2387) Error: invalid namespace xxx +* [#2389](https://github.com/derailed/k9s/issues/2389) Mixed-case named contexts cannot be switched to from contexts view +* [#2382](https://github.com/derailed/k9s/issues/2382) Header always shows Cluster from kubeconfig current-context + +--- + +## Contributed PRs + +Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!! + +* [#2390](https://github.com/derailed/k9s/pull/2390) case sensitive for specific command args and flags + +--- + + © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.30.5.md b/change_logs/release_v0.30.5.md new file mode 100644 index 0000000000..b59cf29af1 --- /dev/null +++ b/change_logs/release_v0.30.5.md @@ -0,0 +1,52 @@ + + +# Release v0.30.5 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## 🎄 Maintenance Release! 🎄 + +Thank you all for pitching in and helping flesh out issues!! + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2394](https://github.com/derailed/k9s/issues/2394) Allow setting custom log dir +* [#2393](https://github.com/derailed/k9s/issues/2393) When switching contexts k9s does not switching to cluster's pod/namespaces/other k8s kinds view +* [#2387](https://github.com/derailed/k9s/issues/2387) Invalid namespace xxx - with feelings! + +--- + +## Contributed PRs + +Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!! + +* [#2396](https://github.com/derailed/k9s/pull/2396) feat: allow to customize logs dir through environment variable +* [#2395](https://github.com/derailed/k9s/pull/2395) fix: create user tmp directory before the app one + +--- + + © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.30.6.md b/change_logs/release_v0.30.6.md new file mode 100644 index 0000000000..51050bd4df --- /dev/null +++ b/change_logs/release_v0.30.6.md @@ -0,0 +1,43 @@ + + +# Release v0.30.6 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## 🎄 Maintenance Release! 🎄 + +Thank you all for pitching in and helping flesh out issues!! + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2401](https://github.com/derailed/k9s/issues/2401) Context completion broken with mixed case context names +* [#2400](https://github.com/derailed/k9s/issues/2400) Panic on start if dns lookup fails +* [#2387](https://github.com/derailed/k9s/issues/2387) Invalid namespace xxx - with feelings?? + +--- + + © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.30.7.md b/change_logs/release_v0.30.7.md new file mode 100644 index 0000000000..b6025dfbea --- /dev/null +++ b/change_logs/release_v0.30.7.md @@ -0,0 +1,51 @@ + + +# Release v0.30.7 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## Maintenance Release! + +Thank you all for pitching in and helping flesh out issues!! + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#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 +* [#2413](https://github.com/derailed/k9s/issues/2413) Typing apply -f in command bar causes k9s to crash +* [#2407](https://github.com/derailed/k9s/issues/2407) Long-running background plugins block UI rendering + +--- + +## Contributed PRs + +Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!! + +* [#2415](https://github.com/derailed/k9s/pull/2415) Add boundary check for args parser +* [#2411](https://github.com/derailed/k9s/pull/2411) Use dash as a standard word separator in skin names + + + © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.30.8.md b/change_logs/release_v0.30.8.md new file mode 100644 index 0000000000..f76143f37d --- /dev/null +++ b/change_logs/release_v0.30.8.md @@ -0,0 +1,48 @@ + + +# Release v0.30.8 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## Maintenance Release! + +Thank you all for pitching in and helping flesh out issues!! + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2423](https://github.com/derailed/k9s/issues/2423) CPU and MEM counters of AKS clusters show not available +* [#2418](https://github.com/derailed/k9s/issues/2418) Boom! runtime error: invalid memory address or nil pointer dereference + +--- + +## Contributed PRs + +Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!! + +* [#2424](https://github.com/derailed/k9s/pull/2424) fix the check for whether the cluster supports metrics + + © 2023 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.31.0.md b/change_logs/release_v0.31.0.md new file mode 100644 index 0000000000..794089ac63 --- /dev/null +++ b/change_logs/release_v0.31.0.md @@ -0,0 +1,153 @@ + + +# Release v0.31.0 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +--- + +## ♫ Sounds Behind The Release ♭ + +* [Border Crossing - Eek A Mouse](https://www.youtube.com/watch?v=KaAC9dBPcOM) +* [The Weight - The Band](https://www.youtube.com/watch?v=FFqb1I-hiHE) +* [Wonderin' - Neil Young](https://www.youtube.com/watch?v=h0PlwVPbM5k) +* [When Your Lover Has Gone - Louis Armstrong](https://www.youtube.com/watch?v=1tdfIj0fvlA) + +--- + +## A Word From Our Sponsors... + +To all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!! + +* [Jacky Nguyen](https://github.com/nktpro) +* [Eckl, Máté](https://github.com/ecklm) +* [Jörgen](https://github.com/wthrbtn) +* [kmath313](https://github.com/kmath313) +* [a-thomas-22](https://github.com/a-thomas-22) +* [wpbeckwith](https://github.com/wpbeckwith) +* [Dima Altukhov](https://github.com/alt-dima) +* [Shoshin Nikita](https://github.com/ShoshinNikita) +* [Tu Hoang](https://github.com/rebyn) +* [Andreas Frangopoulos](https://github.com/qubeio) + +> Sponsorship cancellations since the last release: **7!** 🥹 + +## Feature Release! + +😳 Found a few issues in the neutrino drive... +This is another fairly heavy drop so bracing for impact 😱 +Be sure to dial in the v0.31.0 SneakPeek video below for the gory details! + +😵 Hopefully we've move the needle in the right direction on this drop... 🤞 + +Thank you all for your kindness, feedback and assistance in flushing out issues!! + +### Hold My Hand... + +In this drop, we've added schema validation to ensure various configs are setup as expected. +K9s will now run validation checks on the following configurations: + +1. K9s main configuration (config.yaml) +2. Context specific configs (clusterX/contextY/config.yaml) +3. Skins +4. Aliases +5. HotKeys +6. Plugins +7. Views + +K9s behavior changed in this release if the main configuration does not match schema expectations. +In the past, the configuration will be validated, updated and saved should validation checks failed. Now the app will stop and report validation issues. + +The 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. + +In 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 ;(. + +### Breaking Bad! + +Configuration changes: + +1. DRY fullScreenLogs -> fullScreens (k9s root config.yaml) + + ```yaml + # $XDG_CONFIG_HOME/k9s/config.yaml + k9s: + liveViewAutoRefresh: false + logger: + sinceSeconds: -1 + fullScreen: false # => Was fullScreenLogs + ... + ``` + +2. Views Configuration. + To match other configurations the root is now `views:` vs `k9s: views:` + + ```yaml + # $XDG_CONFIG_HOME/k9s/views.yaml + views: # => Was k9s:\n views: + v1/pods: + columns: + - AGE + - NAMESPACE + ... + ``` + +### Serenity Now! + + 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` + 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. + + ```yaml + # $XDG_CONFIG_HOME/k9s/config.yaml + k9s: + liveViewAutoRefresh: false + UI: + ... + reactive: true # => enable/disable reactive UI + ... + ``` + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE) +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2434](https://github.com/derailed/k9s/issues/2434) readOnly: true in config.yaml doesnt get overriden by readOnly: false in cluster config +* [#2430](https://github.com/derailed/k9s/issues/2430) Referencing a namespace with the name of an alias inside an alias causes infinite loop +* [#2428](https://github.com/derailed/k9s/issues/2428) Boom!! runtime error: invalid memory address or nil pointer dereference - v0.30.8 +* [#2421](https://github.com/derailed/k9s/issues/2421) k9s/config.yaml configuration file is overwritten on launch + +--- + +## Contributed PRs + +Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!! + +* [#2433](https://github.com/derailed/k9s/pull/2433) switch contexts only when needed +* [#2429](https://github.com/derailed/k9s/pull/2429) Reference correct configuration ENV var in README +* [#2426](https://github.com/derailed/k9s/pull/2426) Update carvel plugin kick to shift K +* [#2420](https://github.com/derailed/k9s/pull/2420) supports referencing envs in hotkeys +* [#2419](https://github.com/derailed/k9s/pull/2419) fix typo + + © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.31.1.md b/change_logs/release_v0.31.1.md new file mode 100644 index 0000000000..8f9f730db2 --- /dev/null +++ b/change_logs/release_v0.31.1.md @@ -0,0 +1,157 @@ + + +# Release v0.31.1 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +--- + +## ♫ Sounds Behind The Release ♭ + +* [Border Crossing - Eek A Mouse](https://www.youtube.com/watch?v=KaAC9dBPcOM) +* [The Weight - The Band](https://www.youtube.com/watch?v=FFqb1I-hiHE) +* [Wonderin' - Neil Young](https://www.youtube.com/watch?v=h0PlwVPbM5k) +* [When Your Lover Has Gone - Louis Armstrong](https://www.youtube.com/watch?v=1tdfIj0fvlA) + +--- + +## A Word From Our Sponsors... + +To all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!! + +* [Jacky Nguyen](https://github.com/nktpro) +* [Eckl, Máté](https://github.com/ecklm) +* [Jörgen](https://github.com/wthrbtn) +* [kmath313](https://github.com/kmath313) +* [a-thomas-22](https://github.com/a-thomas-22) +* [wpbeckwith](https://github.com/wpbeckwith) +* [Dima Altukhov](https://github.com/alt-dima) +* [Shoshin Nikita](https://github.com/ShoshinNikita) +* [Tu Hoang](https://github.com/rebyn) +* [Andreas Frangopoulos](https://github.com/qubeio) + +> Sponsorship cancellations since the last release: **7!** 🥹 + +## Feature Release! + +😳 Found a few issues in the neutrino drive... +This is another fairly heavy drop so bracing for impact 😱 +Be sure to dial in the v0.31.0 SneakPeek video below for the gory details! + +😵 Hopefully we've move the needle in the right direction on this drop... 🤞 + +Thank you all for your kindness, feedback and assistance in flushing out issues!! + +> ☢️ Repeating v0.31.0 release notes here as we tweaked the initial drop ☢️ + +### Hold My Hand... + +In this drop, we've added schema validation to ensure various configs are setup as expected. +K9s will now run validation checks on the following configurations: + +1. K9s main configuration (config.yaml) +2. Context specific configs (clusterX/contextY/config.yaml) +3. Skins +4. Aliases +5. HotKeys +6. Plugins +7. Views + +K9s behavior changed in this release if the main configuration does not match schema expectations. +In the past, the configuration will be validated, updated and saved should validation checks failed. Now the app will stop and report validation issues. + +The 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. + +In 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 ;(. + +### Breaking Bad! + +With this release, k9s may not start correctly if the config.yaml configurations are incorrect! + +Configuration changes: + +1. DRY fullScreenLogs -> fullScreens (k9s root config.yaml) + + ```yaml + # $XDG_CONFIG_HOME/k9s/config.yaml + k9s: + liveViewAutoRefresh: false + logger: + sinceSeconds: -1 + fullScreen: false # => Was fullScreenLogs + ... + ``` + +2. Views Configuration. + To match other configurations the root is now `views:` vs `k9s: views:` + + ```yaml + # $XDG_CONFIG_HOME/k9s/views.yaml + views: # => Was k9s:\n views: + v1/pods: + columns: + - AGE + - NAMESPACE + ... + ``` + +### Serenity Now! + + 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` + 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. + + ```yaml + # $XDG_CONFIG_HOME/k9s/config.yaml + k9s: + liveViewAutoRefresh: false + UI: + ... + reactive: true # => enable/disable reactive UI + ... + ``` + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE) +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2434](https://github.com/derailed/k9s/issues/2434) readOnly: true in config.yaml doesnt get overriden by readOnly: false in cluster config +* [#2430](https://github.com/derailed/k9s/issues/2430) Referencing a namespace with the name of an alias inside an alias causes infinite loop +* [#2428](https://github.com/derailed/k9s/issues/2428) Boom!! runtime error: invalid memory address or nil pointer dereference - v0.30.8 +* [#2421](https://github.com/derailed/k9s/issues/2421) k9s/config.yaml configuration file is overwritten on launch + +--- + +## Contributed PRs + +Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!! + +* [#2433](https://github.com/derailed/k9s/pull/2433) switch contexts only when needed +* [#2429](https://github.com/derailed/k9s/pull/2429) Reference correct configuration ENV var in README +* [#2426](https://github.com/derailed/k9s/pull/2426) Update carvel plugin kick to shift K +* [#2420](https://github.com/derailed/k9s/pull/2420) supports referencing envs in hotkeys +* [#2419](https://github.com/derailed/k9s/pull/2419) fix typo + + © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.31.2.md b/change_logs/release_v0.31.2.md new file mode 100644 index 0000000000..68734f5298 --- /dev/null +++ b/change_logs/release_v0.31.2.md @@ -0,0 +1,51 @@ + + +# Release v0.31.2 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## Maintenance Release! + +Yikes! The aftermath... + +Thank you all for pitching in and helping flesh out issues!! + +Please make sure to add gory details to issues ie relevant configs, debug logs, etc... + +Comments 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. +Thank you!! + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE) +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2449](https://github.com/derailed/k9s/issues/2449) [Bug]: views.yaml columns not respected on startup +* [#2448](https://github.com/derailed/k9s/issues/2448) Missing '.thresholds' in config.yaml result in 'assignment to entry in nil map' +* [#2446](https://github.com/derailed/k9s/issues/2446) Context Switch unreliable/not working + +--- + + © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.31.3.md b/change_logs/release_v0.31.3.md new file mode 100644 index 0000000000..00a37b6751 --- /dev/null +++ b/change_logs/release_v0.31.3.md @@ -0,0 +1,52 @@ + + +# Release v0.31.3 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## Maintenance Release! + +The aftermath... + +Thank you all for pitching in and helping flesh out issues!! + +Please make sure to add gory details to issues ie relevant configs, debug logs, etc... + +Comments 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. +Thank you!! + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE) +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2459](https://github.com/derailed/k9s/issues/2459) No permission to see deployments/statefulsets even though I have them +* [#2458](https://github.com/derailed/k9s/issues/2458) panic on run without current context +* [#2454](https://github.com/derailed/k9s/issues/2454) Invoking K9s ends in panic question +* [#2435](https://github.com/derailed/k9s/issues/2435) "yaml: line 15: could not find expected ':'" error bug question (May be??) + +--- + + © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.31.4.md b/change_logs/release_v0.31.4.md new file mode 100644 index 0000000000..6265524911 --- /dev/null +++ b/change_logs/release_v0.31.4.md @@ -0,0 +1,50 @@ + + +# Release v0.31.4 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## Maintenance Release! + +More aftermath... + +Thank you all for pitching in and helping flesh out issues!! + +Please make sure to add gory details to issues ie relevant configs, debug logs, etc... + +Comments like: `same here!` or `me to!` doesn't really help us zero in. +Everyone has slightly different settings/platforms so every little bits of info helps with the resolves. +Thank you!! + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE) +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2463](https://github.com/derailed/k9s/issues/2463) v0.31.3 (Linux_amd64) gives runtime error on startup + +--- + + © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/change_logs/release_v0.31.5.md b/change_logs/release_v0.31.5.md new file mode 100644 index 0000000000..f73584c8d0 --- /dev/null +++ b/change_logs/release_v0.31.5.md @@ -0,0 +1,51 @@ + + +# Release v0.31.5 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## Maintenance Release! + +😱 More aftermath... 😱 + +Thank you all for pitching in and helping flesh out issues!! + +Please make sure to add gory details to issues ie relevant configs, debug logs, etc... + +Comments like: `same here!` or `me to!` doesn't really cut it for us to zero in ;( +Everyone has slightly different settings/platforms so every little bits of info helps with the resolves even if seemingly irrelevant. +Thank you!! + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE) +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2466](https://github.com/derailed/k9s/issues/2466) Panic: index out of range [0] with length 0 +* [#2465](https://github.com/derailed/k9s/issues/2465) v0.31.4 - panic; no client connection detected - with feelings!! + +--- + + © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) \ No newline at end of file diff --git a/change_logs/release_v0.31.6.md b/change_logs/release_v0.31.6.md new file mode 100644 index 0000000000..890e6aca5a --- /dev/null +++ b/change_logs/release_v0.31.6.md @@ -0,0 +1,75 @@ + + +# Release v0.31.6 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## Maintenance Release! + +😱 More aftermath... 😱 + +Thank you all for pitching in and helping flesh out issues!! + +Please make sure to add gory details to issues ie relevant configs, debug logs, etc... + +Comments like: `same here!` or `me to!` doesn't really cut it for us to zero in ;( +Everyone has slightly different settings/platforms so every little bits of info helps with the resolves even if seemingly irrelevant. + +--- + +## NOTE + +In 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. +Please double check your k9s logs if things don't work as expected and file an issue with the `gory` details! + +☢️ This drop may cause `some disturbance in the farce!` ☢️ + +Please proceed with caution with this one as we did our best to attempt to address potential context config file corruption by eliminating race conditions. +It's late and I am operating on minimal sleep so I may have hosed some behaviors 🫣 +If you experience k9s locking up or misbehaving, as per the above👆 you know what to do now and as customary +we will do our best to address them quickly to get you back up and running! + +Thank you for your support, kindness and patience! + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE) +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2476](https://github.com/derailed/k9s/issues/2476) Pod's are not displayed for the selected namespace. Hopefully! +* [#2471](https://github.com/derailed/k9s/issues/2471) Shell autocomplete functions do not work correctly + +--- + +## Contributed PRs + +Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!! + +* [#2480](https://github.com/derailed/k9s/pull/2480) Adding system arch to nodes view +* [#2477](https://github.com/derailed/k9s/pull/2477) Shell autocomplete for k8s flags + +--- + + © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) \ No newline at end of file diff --git a/change_logs/release_v0.31.7.md b/change_logs/release_v0.31.7.md new file mode 100644 index 0000000000..ac4179c8c5 --- /dev/null +++ b/change_logs/release_v0.31.7.md @@ -0,0 +1,49 @@ + + +# Release v0.31.7 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## Maintenance Release! + +😱 More aftermath... 😱 + +Thank you all for pitching in and helping flesh out issues!! + +Please make sure to add gory details to issues ie relevant configs, debug logs, etc... + +Comments like: `same here!` or `me to!` doesn't really cut it for us to zero in ;( +Everyone has slightly different settings/platforms so every little bits of info helps with the resolves even if seemingly irrelevant. + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE) +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2488](https://github.com/derailed/k9s/issues/2488) linux_amd64 "--kubeconfig" not working on v0.31.6 + +--- + + © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) \ No newline at end of file diff --git a/change_logs/release_v0.31.8.md b/change_logs/release_v0.31.8.md new file mode 100644 index 0000000000..f17473e165 --- /dev/null +++ b/change_logs/release_v0.31.8.md @@ -0,0 +1,102 @@ + + +# Release v0.31.8 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## Maintenance Release! + +Thank you all for pitching in and helping flesh out issues!! + +Please make sure to add gory details to issues ie relevant configs, debug logs, etc... + +Comments like: `same here!` or `me to!` doesn't really cut it for us to zero in ;( +Everyone has slightly different settings/platforms so every little bits of info helps with the resolves even if seemingly irrelevant. + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE) +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## ♫ Sounds Behind The Release ♭ + +Going back to the classics... + +* [Ambulance Blues - Neil Young](https://www.youtube.com/watch?v=bCQisTEdBwY) +* [Christopher Columbus - Burning Spear](https://www.youtube.com/watch?v=5qbMKTY_Cr0) +* [Feelin' the Same - Clinton Fearon](https://www.youtube.com/watch?v=aRPF2Yta_cs) + +--- + +## A Word From Our Sponsors... + +To all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!! + +* [Andreas Frangopoulos](https://github.com/qubeio) +* [Tu Hoang](https://github.com/rebyn) +* [Shoshin Nikita](https://github.com/ShoshinNikita) +* [Dima Altukhov](https://github.com/alt-dima) +* [wpbeckwith](https://github.com/wpbeckwith) +* [a-thomas-22](https://github.com/a-thomas-22) +* [kmath313](https://github.com/kmath313) +* [Jörgen](https://github.com/wthrbtn) +* [Eckl, Máté](https://github.com/ecklm) +* [Jacky Nguyen](https://github.com/nktpro) +* [Chris Bradley](https://github.com/chrisbradleydev) +* [Vytautas Kubilius](https://github.com/vytautaskubilius) +* [Patrick Christensen](https://github.com/BuriedStPatrick) +* [Ollie Lowson](https://github.com/ollielowson-wcbs) +* [Mike Macaulay](https://github.com/mmacaula) +* [David Birks](https://github.com/dbirks) +* [James Hounshell](https://github.com/jameshounshell) +* [elapse2039](https://github.com/elapse2039) +* [Vinicius Xavier](https://github.com/vinixaavier) +* [Phuc Phung](https://github.com/Foxhound401) +* [ollielowson](https://github.com/ollielowson) + +> Sponsorship cancellations since the last release: **4!** 🥹 + +--- + +## Resolved Issues + +* [#2527](https://github.com/derailed/k9s/issues/2527) Multiple k9s panels open in parallel for the same cluster breaks config.yaml +* [#2520](https://github.com/derailed/k9s/issues/2520) pods with init container with restartPolicy: Always stay in Init status +* [#2501](https://github.com/derailed/k9s/issues/2501) Cannot add plugins to helm scope bug +* [#2492](https://github.com/derailed/k9s/issues/2492) API Resources "carry over" between contexts, causing errors if they share shortnames +* [#1158](https://github.com/derailed/k9s/issues/1158) Removing a helm release incorrectly determines the namespace of resources +* [#1033](https://github.com/derailed/k9s/issues/1033) Helm delete deletes only the helm entry but not the deployment + +--- + +## Contributed PRs + +Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!! + +* [#2509](https://github.com/derailed/k9s/pull/2509) Fix Toggle Faults filtering +* [#2511](https://github.com/derailed/k9s/pull/2511) adding the f command to pf extender view +* [#2518](https://github.com/derailed/k9s/pull/2518) Added defaultsToFullScreen flag for Live/Details view,logs + +--- + + © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) \ No newline at end of file diff --git a/change_logs/release_v0.31.9.md b/change_logs/release_v0.31.9.md new file mode 100644 index 0000000000..25273f8813 --- /dev/null +++ b/change_logs/release_v0.31.9.md @@ -0,0 +1,98 @@ + + +# Release v0.31.9 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## Maintenance Release! + +```text +S .-'-. + o __| F `\ + S `-,-`--._ `\ + [] .->' X `|-' + `=/ (__/_ / + \_, ` _) + `----; | +``` + +⛔️ WE HAVE A PIPER DOWN! I REPEAT PIPER IS DOWN!! ⛔️ + +Popeye is undergoing heavy surgery at the moment so I had to break the bridge. +If you dig Popeye please run the binary separately for the time being. +I'll post another message here once the spinach formula upgrade is successful! + +Also please make sure to add the gory details to issues ie relevant configs, debug logs, etc... +Comments like: `same here!` or `me to!` doesn't really cut it for us to zero in ;( + +Everyone has slightly different settings/platforms so every little bits of info helps with the resolves even if seemingly irrelevant. + +Thank you all for pitching in and helping flesh out issues!! + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE) +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## ♫ Sounds Behind The Release ♭ + +Ushered or Taylored out? + +* [Rough God Goes Riding - Van Morrison](https://www.youtube.com/watch?v=-kGrwRlJxcM) +* [Walk On - John Hiatt](https://www.youtube.com/watch?v=YVdMyeTQCkw) +* [On The Beach - Neil Young](https://www.youtube.com/watch?v=KBVde75e4sU) + +--- + +## A Word From Our Sponsors... + +To all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!! + +* [Francis Lalonde](https://github.com/f-lalonde) +* [e-conomic a/s](https://github.com/e-conomic) + +> Sponsorship cancellations since the last release: **2!** 🥹 + +--- + +## Resolved Issues + +* [#2540](https://github.com/derailed/k9s/issues/2540) Option --write not functional +* [#2538](https://github.com/derailed/k9s/issues/2538) Opening screen dumps (sd) in K9s results in Failed to launch editor error message +* [#2536](https://github.com/derailed/k9s/issues/2536) Recent namespaces are lost when changing context +* [#2535](https://github.com/derailed/k9s/issues/2535) Namespaced configmap edit fails for user with RoleBinding to a role that allows it +* [#2532](https://github.com/derailed/k9s/issues/2532) Sporadic crashes (Maybe??) + +--- + +## Contributed PRs + +Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!! + +* [#2541](https://github.com/derailed/k9s/pull/2541) Add Rose Pine moon and dawn variants to skins +* [#2531](https://github.com/derailed/k9s/pull/2531) fix the --write flag +* [#2516](https://github.com/derailed/k9s/pull/2516) Added defaultsToFullScreen flag for Live/Details view,logs + +--- + + © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) \ No newline at end of file diff --git a/change_logs/release_v0.32.0.md b/change_logs/release_v0.32.0.md new file mode 100644 index 0000000000..e35c3b61f5 --- /dev/null +++ b/change_logs/release_v0.32.0.md @@ -0,0 +1,73 @@ + + +# Release v0.32.0 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## Maintenance Release! + +A lot of refactors, perf improvements (crossing fingers+toes!) and general spring cleaning items in this release. +Thus I expect a bit of `disturbance in the farce` given the major code churns, so please beware! + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE) +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## A Word From Our Sponsors... + +To all the good folks below that opted to `pay it forward` and join our sponsorship program, I salute you!! + +* [Justin Reid](https://github.com/jmreid) +* [Danni](https://github.com/danninov) +* [Robert Krahn](https://github.com/rksm) +* [Hao Ke](https://github.com/kehao95) +* [PH](https://github.com/raphael-com-ph) + +> Sponsorship cancellations since the last release: **9!!** 🥹 + +--- + +## Resolved Issues + +* [#2569](https://github.com/derailed/k9s/issues/2569) k9s panics on start if the main config file (config.yml) is owned by root +* [#2568](https://github.com/derailed/k9s/issues/2568) kube context in running k9s is no longer sticky, during kubectx context switch +* [#2560](https://github.com/derailed/k9s/issues/2560) Namespace/Settings keeps resetting +* [#2557](https://github.com/derailed/k9s/issues/2557) [Feature]: Sort CRDs by their group +* [#1462](https://github.com/derailed/k9s/issues/1462) k9s running very slowly when opening namespace with 13k pods (maybe??) + +--- + +## Contributed PRs + +Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!! + +* [#2564](https://github.com/derailed/k9s/pull/2564) Add everforest skins +* [#2558](https://github.com/derailed/k9s/pull/2558) feat: sort by role in node list view +* [#2554](https://github.com/derailed/k9s/pull/2554) Added context to the debug command for debug-container plugin +* [#2554](https://github.com/derailed/k9s/pull/2554) Correctly respect the KUBECACHEDIR env var +* [#2546](https://github.com/derailed/k9s/pull/2546) Use configured log fgColor to print log markers + +--- + + © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) \ No newline at end of file diff --git a/change_logs/release_v0.32.1.md b/change_logs/release_v0.32.1.md new file mode 100644 index 0000000000..6018dd45e2 --- /dev/null +++ b/change_logs/release_v0.32.1.md @@ -0,0 +1,51 @@ + + +# Release v0.32.1 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## Maintenance Release! + +The aftermath ;( + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE) +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2584](https://github.com/derailed/k9s/issues/2584) Transfer of file doesn't detect corruption +* [#2579](https://github.com/derailed/k9s/issues/2579) Default sorting behavior changed to descending sort bug + +--- + +## Contributed PRs + +Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!! + +* [#2586](https://github.com/derailed/k9s/pull/2586) Properly initialize key actions in picker + +--- + + © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) \ No newline at end of file diff --git a/change_logs/release_v0.32.2.md b/change_logs/release_v0.32.2.md new file mode 100644 index 0000000000..96affe5958 --- /dev/null +++ b/change_logs/release_v0.32.2.md @@ -0,0 +1,43 @@ + + +# Release v0.32.2 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## Maintenance Release! + +Mo aftermath ;( + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE) +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2582](https://github.com/derailed/k9s/issues/2582) Slowness due to client-side throttling in v0.32.0 (Maybe??) +* [#2593](https://github.com/derailed/k9s/issues/2593) Popeye not working in 0.32.X + +--- + + © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) \ No newline at end of file diff --git a/change_logs/release_v0.32.3.md b/change_logs/release_v0.32.3.md new file mode 100644 index 0000000000..c4d85c6b4d --- /dev/null +++ b/change_logs/release_v0.32.3.md @@ -0,0 +1,42 @@ + + +# Release v0.32.3 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## Maintenance Release! + +Look like v0.32.2 drop release bins are toast. So m'o aftermath ;( + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE) +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2584](https://github.com/derailed/k9s/issues/2584) Transfer of file doesn't detect corruption (with feelings!) + +--- + + © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) \ No newline at end of file diff --git a/change_logs/release_v0.32.4.md b/change_logs/release_v0.32.4.md new file mode 100644 index 0000000000..15d012321d --- /dev/null +++ b/change_logs/release_v0.32.4.md @@ -0,0 +1,65 @@ + + +# Release v0.32.4 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## Maintenance Release! + +--- + +## ♫ Sounds Behind The Release ♭ + +Thinking of all you at KubeCon Paris!! +May I suggest a nice glass of `cold Merlote` or other fine grape juices from my country? + +* [Le Gorille - George Brassens](https://www.youtube.com/watch?v=KVfwvk_yVyA) +* [Les Funerailles D'antan (Love this guy!) - George Brassens](https://www.youtube.com/watch?v=bwb5k4k2EMc) +* [Poinconneur Des Lilas - Serge Gainsbourg](https://www.youtube.com/watch?v=eWkWCFzkOvU) +* [Mon Legionaire (Yup! same guy??) - Serge Gainsbourg](https://www.youtube.com/watch?v=gl8gopryqWI) +* [Les Cornichons - Nino Ferrer](https://www.youtube.com/watch?v=N7JSW4NhM8I) +* [Paris s'eveille - Jacques Dutronc](https://www.youtube.com/watch?v=3WcCg6rm3uM) + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE) +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2608](https://github.com/derailed/k9s/issues/2608) Make the sanitize feature easier to use +* [#2605](https://github.com/derailed/k9s/issues/2605) Built-in shortcuts being overridden by plugins result in excessive logging +* [#2604](https://github.com/derailed/k9s/issues/2604) Ability to mark a plugin as Dangerous/destructive +* [#2592](https://github.com/derailed/k9s/issues/2592) "list access denied" when switching contexts within k9s since 0.32.0 + +--- + +## Contributed PRs + +Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!! + +* [#2621](https://github.com/derailed/k9s/pull/2621) Fix snap build + +--- + + © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) \ No newline at end of file diff --git a/change_logs/release_v0.32.5.md b/change_logs/release_v0.32.5.md new file mode 100644 index 0000000000..0948e3a5c2 --- /dev/null +++ b/change_logs/release_v0.32.5.md @@ -0,0 +1,64 @@ + + +# Release v0.32.5 + +## Notes + +Thank 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!! + +As 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) + +On Slack? Please join us [K9slackers](https://join.slack.com/t/k9sers/shared_invite/enQtOTA5MDEyNzI5MTU0LWQ1ZGI3MzliYzZhZWEyNzYxYzA3NjE0YTk1YmFmNzViZjIyNzhkZGI0MmJjYzhlNjdlMGJhYzE2ZGU1NjkyNTM) + +## Maintenance Release! + +--- + +## Videos Are In The Can! + +Please dial [K9s Channel](https://www.youtube.com/channel/UC897uwPygni4QIjkPCpgjmw) for up coming content... + +* [K9s v0.31.0 Configs+Sneak peek](https://youtu.be/X3444KfjguE) +* [K9s v0.30.0 Sneak peek](https://youtu.be/mVBc1XneRJ4) +* [Vulnerability Scans](https://youtu.be/ULkl0MsaidU) + +--- + +## Resolved Issues + +* [#2734](https://github.com/derailed/k9s/issues/2734) Incorrect pod containers displayed when using custom resource columns +* [#2733](https://github.com/derailed/k9s/issues/2733) Toggle Wide and Toggle Faults broken for PDB view +* [#2656](https://github.com/derailed/k9s/issues/2656) nil pointer dereference when switching contexts +* [#2617](https://github.com/derailed/k9s/issues/2617) Plugin command execution output + +--- + +## Contributed PRs + +Please be sure to give `Big Thanks!` and `ATTA Girls/Boys!` to all the fine contributors for making K9s better for all of us!! + +* [#2736](https://github.com/derailed/k9s/pull/2736) fix view sorting being reset +* [#2732](https://github.com/derailed/k9s/pull/2732) use policy/v1 instead of policy/v1beta1 +* [#2728](https://github.com/derailed/k9s/pull/2728) feat: add pool col to node view +* [#2718](https://github.com/derailed/k9s/pull/2718) fix: jump to namespaceless owner reference +* [#2711](https://github.com/derailed/k9s/pull/2711) Add plugins for argo-rollouts +* [#2700](https://github.com/derailed/k9s/pull/2700) feat: allow jumping to the owner of the resource +* [#2699](https://github.com/derailed/k9s/pull/2699) Added cert-manager and openssl plugins +* [#2711](https://github.com/derailed/k9s/pull/2711) Add plugins for argo-rollouts +* [#2698](https://github.com/derailed/k9s/pull/2698) fix: job color based on failures (#2686) +* [#2685](https://github.com/derailed/k9s/pull/2685) feat: support cluster and cmp view +* [#2678](https://github.com/derailed/k9s/pull/2678) fix: do not hard-code path to kubectl in jq plugin +* [#2676](https://github.com/derailed/k9s/pull/2676) Add kanagawa skin +* [#2666](https://github.com/derailed/k9s/pull/2666) save config when closing k9s with ctrl-c +* [#2644](https://github.com/derailed/k9s/pull/2644) Allow overwriting plugin output with command's stdout + +--- + + © 2024 Imhotep Software LLC. All materials licensed under [Apache v2.0](http://www.apache.org/licenses/LICENSE-2.0) \ No newline at end of file diff --git a/cmd/info.go b/cmd/info.go index 9828aab286..8fbb01e500 100644 --- a/cmd/info.go +++ b/cmd/info.go @@ -1,8 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package cmd import ( "fmt" - "os" "github.com/derailed/k9s/internal/color" @@ -16,21 +18,31 @@ import ( func infoCmd() *cobra.Command { return &cobra.Command{ Use: "info", - Short: "Print configuration info", - Long: "Print configuration information", - Run: func(cmd *cobra.Command, args []string) { - printInfo() - }, + Short: "List K9s configurations info", + RunE: printInfo, } } -func printInfo() { - const fmat = "%-25s %s\n" +func printInfo(cmd *cobra.Command, args []string) error { + if err := config.InitLocs(); err != nil { + return err + } + const fmat = "%-27s %s\n" printLogo(color.Cyan) - printTuple(fmat, "Configuration", config.K9sConfigFile, color.Cyan) - printTuple(fmat, "Logs", config.DefaultLogFile, color.Cyan) - printTuple(fmat, "Screen Dumps", getScreenDumpDirForInfo(), color.Cyan) + printTuple(fmat, "Version", version, color.Cyan) + printTuple(fmat, "Config", config.AppConfigFile, color.Cyan) + printTuple(fmat, "Custom Views", config.AppViewsFile, color.Cyan) + printTuple(fmat, "Plugins", config.AppPluginsFile, color.Cyan) + printTuple(fmat, "Hotkeys", config.AppHotKeysFile, color.Cyan) + printTuple(fmat, "Aliases", config.AppAliasesFile, color.Cyan) + printTuple(fmat, "Skins", config.AppSkinsDir, color.Cyan) + printTuple(fmat, "Context Configs", config.AppContextsDir, color.Cyan) + printTuple(fmat, "Logs", config.AppLogFile, color.Cyan) + printTuple(fmat, "Benchmarks", config.AppBenchmarksDir, color.Cyan) + printTuple(fmat, "ScreenDumps", getScreenDumpDirForInfo(), color.Cyan) + + return nil } func printLogo(c color.Paint) { @@ -42,24 +54,24 @@ func printLogo(c color.Paint) { // getScreenDumpDirForInfo get default screen dump config dir or from config.K9sConfigFile configuration. func getScreenDumpDirForInfo() string { - if config.K9sConfigFile == "" { - return config.K9sDefaultScreenDumpDir + if config.AppConfigFile == "" { + return config.AppDumpsDir } - f, err := os.ReadFile(config.K9sConfigFile) + f, err := os.ReadFile(config.AppConfigFile) if err != nil { log.Error().Err(err).Msgf("Reads k9s config file %v", err) - return config.K9sDefaultScreenDumpDir + return config.AppDumpsDir } var cfg config.Config if err := yaml.Unmarshal(f, &cfg); err != nil { log.Error().Err(err).Msgf("Unmarshal k9s config %v", err) - return config.K9sDefaultScreenDumpDir + return config.AppDumpsDir } if cfg.K9s == nil { - cfg.K9s = config.NewK9s() + return config.AppDumpsDir } - return cfg.K9s.GetScreenDumpDir() + return cfg.K9s.AppScreenDumpDir() } diff --git a/cmd/info_test.go b/cmd/info_test.go index 867d34789e..e181848502 100644 --- a/cmd/info_test.go +++ b/cmd/info_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package cmd import ( @@ -13,32 +16,31 @@ func Test_getScreenDumpDirForInfo(t *testing.T) { expectedScreenDumpDir string }{ "withK9sConfigFile": { - k9sConfigFile: "testdata/k9s.yml", + k9sConfigFile: "testdata/k9s.yaml", expectedScreenDumpDir: "/tmp", }, "withEmptyK9sConfigFile": { k9sConfigFile: "", - expectedScreenDumpDir: config.K9sDefaultScreenDumpDir, + expectedScreenDumpDir: config.AppDumpsDir, }, "withInvalidK9sConfigFilePath": { k9sConfigFile: "invalid", - expectedScreenDumpDir: config.K9sDefaultScreenDumpDir, + expectedScreenDumpDir: config.AppDumpsDir, }, "withScreenDumpDirEmptyInK9sConfigFile": { - k9sConfigFile: "testdata/k9s1.yml", - expectedScreenDumpDir: config.K9sDefaultScreenDumpDir, + k9sConfigFile: "testdata/k9s1.yaml", + expectedScreenDumpDir: config.AppDumpsDir, }, } for k := range tests { u := tests[k] t.Run(k, func(t *testing.T) { - initK9sConfigFile := config.K9sConfigFile - - config.K9sConfigFile = u.k9sConfigFile + initK9sConfigFile := config.AppConfigFile + config.AppConfigFile = u.k9sConfigFile assert.Equal(t, u.expectedScreenDumpDir, getScreenDumpDirForInfo()) - config.K9sConfigFile = initK9sConfigFile + config.AppConfigFile = initK9sConfigFile }) } } diff --git a/cmd/root.go b/cmd/root.go index 357e42b07f..5181c8bd7a 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,9 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package cmd import ( + "errors" "fmt" "os" "runtime/debug" + "strings" + + "github.com/derailed/k9s/internal/config/data" + "k8s.io/client-go/tools/clientcmd/api" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/color" @@ -17,12 +25,12 @@ import ( ) const ( - appName = "k9s" + appName = config.AppName shortAppDesc = "A graphical CLI for your Kubernetes cluster management." longAppDesc = "K9s is a CLI to view and manage your Kubernetes clusters." ) -var _ config.KubeSettings = (*client.Config)(nil) +var _ data.KubeSettings = (*client.Config)(nil) var ( version, commit, date = "dev", "dev", client.NA @@ -39,7 +47,19 @@ var ( out = colorable.NewColorableStdout() ) +type flagError struct{ err error } + +func (e flagError) Error() string { return e.err.Error() } + func init() { + if err := config.InitLogLoc(); err != nil { + fmt.Printf("Fail to init k9s logs location %s\n", err) + } + + rootCmd.SetFlagErrorFunc(func(command *cobra.Command, err error) error { + return flagError{err: err} + }) + rootCmd.AddCommand(versionCmd(), infoCmd()) initK9sFlags() initK8sFlags() @@ -48,18 +68,23 @@ func init() { // Execute root command. func Execute() { if err := rootCmd.Execute(); err != nil { - log.Panic().Err(err) + if !errors.As(err, &flagError{}) { + panic(err) + } } } func run(cmd *cobra.Command, args []string) error { - if err := config.EnsureDirPath(*k9sFlags.LogFile, config.DefaultDirMod); err != nil { + if err := config.InitLocs(); err != nil { return err } - mod := os.O_CREATE | os.O_APPEND | os.O_WRONLY - file, err := os.OpenFile(*k9sFlags.LogFile, mod, config.DefaultFileMod) + file, err := os.OpenFile( + *k9sFlags.LogFile, + os.O_CREATE|os.O_APPEND|os.O_WRONLY, + data.DefaultFileMod, + ) if err != nil { - return err + return fmt.Errorf("Log file %q init failed: %w", *k9sFlags.LogFile, err) } defer func() { if file != nil { @@ -77,9 +102,13 @@ func run(cmd *cobra.Command, args []string) error { }() log.Logger = log.Output(zerolog.ConsoleWriter{Out: file}) - zerolog.SetGlobalLevel(parseLevel(*k9sFlags.LogLevel)) - app := view.NewApp(loadConfiguration()) + + cfg, err := loadConfiguration() + if err != nil { + log.Error().Err(err).Msgf("Fail to load global/context configuration") + } + app := view.NewApp(cfg) if err := app.Init(version, *k9sFlags.RefreshRate); err != nil { return err } @@ -93,51 +122,41 @@ func run(cmd *cobra.Command, args []string) error { return nil } -func loadConfiguration() *config.Config { +func loadConfiguration() (*config.Config, error) { log.Info().Msg("🐶 K9s starting up...") - // Load K9s config file... k8sCfg := client.NewConfig(k8sFlags) k9sCfg := config.NewConfig(k8sCfg) - - if err := k9sCfg.Load(config.K9sConfigFile); err != nil { - log.Warn().Msg("Unable to locate K9s config. Generating new configuration...") + var errs error + conn, err := client.InitConnection(k8sCfg) + k9sCfg.SetConnection(conn) + if err != nil { + errs = errors.Join(errs, err) } - if *k9sFlags.RefreshRate != config.DefaultRefreshRate { - k9sCfg.K9s.OverrideRefreshRate(*k9sFlags.RefreshRate) + if err := k9sCfg.Load(config.AppConfigFile, false); err != nil { + errs = errors.Join(errs, err) } - - k9sCfg.K9s.OverrideHeadless(*k9sFlags.Headless) - k9sCfg.K9s.OverrideLogoless(*k9sFlags.Logoless) - k9sCfg.K9s.OverrideCrumbsless(*k9sFlags.Crumbsless) - k9sCfg.K9s.OverrideReadOnly(*k9sFlags.ReadOnly) - k9sCfg.K9s.OverrideWrite(*k9sFlags.Write) - k9sCfg.K9s.OverrideCommand(*k9sFlags.Command) - k9sCfg.K9s.OverrideScreenDumpDir(*k9sFlags.ScreenDumpDir) - + k9sCfg.K9s.Override(k9sFlags) if err := k9sCfg.Refine(k8sFlags, k9sFlags, k8sCfg); err != nil { - log.Error().Err(err).Msgf("refine failed") - } - conn, err := client.InitConnection(k8sCfg) - k9sCfg.SetConnection(conn) - if err != nil { - log.Error().Err(err).Msgf("failed to connect to cluster %q", k9sCfg.K9s.CurrentContext) - return k9sCfg + log.Error().Err(err).Msgf("config refine failed") + errs = errors.Join(errs, err) } // Try to access server version if that fail. Connectivity issue? - if !k9sCfg.GetConnection().CheckConnectivity() { - log.Panic().Msgf("Cannot connect to cluster %s", k9sCfg.K9s.CurrentCluster) + if !conn.CheckConnectivity() { + errs = errors.Join(errs, fmt.Errorf("cannot connect to context: %s", k9sCfg.K9s.ActiveContextName())) } - if !k9sCfg.GetConnection().ConnectionOK() { - panic("No connectivity") + if !conn.ConnectionOK() { + errs = errors.Join(errs, fmt.Errorf("k8s connection failed for context: %s", k9sCfg.K9s.ActiveContextName())) } + log.Info().Msg("✅ Kubernetes connectivity") - if err := k9sCfg.Save(); err != nil { + if err := k9sCfg.Save(false); err != nil { log.Error().Err(err).Msg("Config save") + errs = errors.Join(errs, err) } - return k9sCfg + return k9sCfg, errs } func parseLevel(level string) zerolog.Level { @@ -174,7 +193,7 @@ func initK9sFlags() { rootCmd.Flags().StringVarP( k9sFlags.LogFile, "logFile", "", - config.DefaultLogFile, + config.AppLogFile, "Specify the log file", ) rootCmd.Flags().BoolVar( @@ -276,6 +295,7 @@ func initK8sFlags() { initAsFlags() initCertFlags() + initK8sFlagCompletion() } func initAsFlags() { @@ -330,3 +350,56 @@ func initCertFlags() { "Bearer token for authentication to the API server", ) } + +type ( + k8sPickerFn[T any] func(cfg *api.Config) map[string]T + completeFn func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) +) + +func initK8sFlagCompletion() { + _ = rootCmd.RegisterFlagCompletionFunc("context", k8sFlagCompletion(func(cfg *api.Config) map[string]*api.Context { + return cfg.Contexts + })) + + _ = rootCmd.RegisterFlagCompletionFunc("cluster", k8sFlagCompletion(func(cfg *api.Config) map[string]*api.Cluster { + return cfg.Clusters + })) + + _ = rootCmd.RegisterFlagCompletionFunc("user", k8sFlagCompletion(func(cfg *api.Config) map[string]*api.AuthInfo { + return cfg.AuthInfos + })) + + _ = rootCmd.RegisterFlagCompletionFunc("namespace", func(cmd *cobra.Command, args []string, s string) ([]string, cobra.ShellCompDirective) { + conn := client.NewConfig(k8sFlags) + if c, err := client.InitConnection(conn); err == nil { + if nss, err := c.ValidNamespaceNames(); err == nil { + return filterFlagCompletions(nss, s) + } + } + + return nil, cobra.ShellCompDirectiveError + }) +} + +func k8sFlagCompletion[T any](picker k8sPickerFn[T]) completeFn { + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + conn := client.NewConfig(k8sFlags) + cfg, err := conn.RawConfig() + if err != nil { + log.Error().Err(err).Msgf("k8s config getter failed") + } + + return filterFlagCompletions(picker(&cfg), toComplete) + } +} + +func filterFlagCompletions[T any](m map[string]T, s string) ([]string, cobra.ShellCompDirective) { + cc := make([]string, 0, len(m)) + for name := range m { + if strings.HasPrefix(name, s) { + cc = append(cc, name) + } + } + + return cc, cobra.ShellCompDirectiveNoFileComp +} diff --git a/cmd/testdata/k9s.yml b/cmd/testdata/k9s.yaml similarity index 100% rename from cmd/testdata/k9s.yml rename to cmd/testdata/k9s.yaml diff --git a/cmd/testdata/k9s1.yml b/cmd/testdata/k9s1.yaml similarity index 100% rename from cmd/testdata/k9s1.yml rename to cmd/testdata/k9s1.yaml diff --git a/cmd/version.go b/cmd/version.go index 13617a4801..67727c7d23 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package cmd import ( diff --git a/go.mod b/go.mod index b244e8b4ff..a0e5bbd5a9 100644 --- a/go.mod +++ b/go.mod @@ -1,165 +1,329 @@ module github.com/derailed/k9s -go 1.21 +go 1.22.0 require ( github.com/adrg/xdg v0.4.0 + github.com/anchore/clio v0.0.0-20240209204744-cb94e40a4f65 + github.com/anchore/grype v0.77.0 + github.com/anchore/syft v1.2.0 github.com/atotto/clipboard v0.1.4 - github.com/cenkalti/backoff/v4 v4.2.1 - github.com/derailed/popeye v0.11.1 + github.com/cenkalti/backoff/v4 v4.3.0 + github.com/derailed/popeye v0.11.3 github.com/derailed/tcell/v2 v2.3.1-rc.3 - github.com/derailed/tview v0.8.1 + github.com/derailed/tview v0.8.3 github.com/fatih/color v1.16.0 github.com/fsnotify/fsnotify v1.7.0 github.com/fvbommel/sortorder v1.1.0 + github.com/go-errors/errors v1.4.2 github.com/mattn/go-colorable v0.1.13 - github.com/mattn/go-runewidth v0.0.14 + github.com/mattn/go-runewidth v0.0.15 + github.com/olekukonko/tablewriter v0.0.5 github.com/petergtz/pegomock v2.9.0+incompatible github.com/rakyll/hey v0.1.4 - github.com/rs/zerolog v1.31.0 - github.com/sahilm/fuzzy v0.1.0 + github.com/rs/zerolog v1.32.0 + github.com/sahilm/fuzzy v0.1.1 github.com/spf13/cobra v1.8.0 - github.com/stretchr/testify v1.8.4 - golang.org/x/text v0.13.0 + github.com/stretchr/testify v1.9.0 + github.com/xeipuuv/gojsonschema v1.2.0 + golang.org/x/text v0.14.0 gopkg.in/yaml.v2 v2.4.0 - helm.sh/helm/v3 v3.13.1 - k8s.io/api v0.28.3 - k8s.io/apiextensions-apiserver v0.28.3 - k8s.io/apimachinery v0.28.3 - k8s.io/cli-runtime v0.28.3 - k8s.io/client-go v0.28.3 - k8s.io/klog/v2 v2.100.1 - k8s.io/kubectl v0.28.3 - k8s.io/kubernetes v1.28.3 - k8s.io/metrics v0.28.3 - sigs.k8s.io/yaml v1.3.0 + gopkg.in/yaml.v3 v3.0.1 + helm.sh/helm/v3 v3.14.4 + k8s.io/api v0.30.1 + k8s.io/apiextensions-apiserver v0.30.1 + k8s.io/apimachinery v0.30.1 + k8s.io/cli-runtime v0.30.1 + k8s.io/client-go v0.30.1 + k8s.io/klog/v2 v2.120.1 + k8s.io/kubectl v0.30.1 + k8s.io/metrics v0.30.1 + sigs.k8s.io/yaml v1.4.0 ) require ( + cloud.google.com/go v0.110.10 // indirect + cloud.google.com/go/compute v1.23.3 // indirect + cloud.google.com/go/compute/metadata v0.2.3 // indirect + cloud.google.com/go/iam v1.1.5 // indirect + cloud.google.com/go/storage v1.35.1 // indirect + dario.cat/mergo v1.0.0 // indirect github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect - github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect + github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/BurntSushi/toml v1.3.2 // indirect + github.com/CycloneDX/cyclonedx-go v0.8.0 // indirect + github.com/DataDog/zstd v1.5.5 // indirect github.com/MakeNowJust/heredoc v1.0.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect - github.com/Microsoft/hcsshim v0.11.0 // indirect + github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/Microsoft/hcsshim v0.11.4 // indirect + github.com/ProtonMail/go-crypto v1.0.0 // indirect + github.com/acobaugh/osrelease v0.1.0 // indirect + github.com/anchore/fangs v0.0.0-20231201140849-5075d28d6d8b // indirect + github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537 // indirect + github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a // indirect + github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb // indirect + github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect + github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 // indirect + github.com/anchore/packageurl-go v0.1.1-0.20240312213626-055233e539b4 // indirect + github.com/anchore/stereoscope v0.0.2-0.20240229175558-fe426d1b1c84 // indirect + github.com/andybalholm/brotli v1.0.4 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 // indirect + github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 // indirect github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 // indirect - github.com/aws/aws-sdk-go v1.38.49 // indirect + github.com/aws/aws-sdk-go v1.44.288 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/becheran/wildmatch-go v1.0.0 // indirect github.com/beorn7/perks v1.0.1 // indirect + github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect + github.com/bmatcuk/doublestar/v2 v2.0.4 // indirect + github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect - github.com/containerd/containerd v1.7.6 // indirect + github.com/charmbracelet/lipgloss v0.10.0 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/containerd/cgroups v1.1.0 // indirect + github.com/containerd/containerd v1.7.12 // indirect + github.com/containerd/continuity v0.4.2 // indirect + github.com/containerd/fifo v1.1.0 // indirect + github.com/containerd/log v0.1.0 // indirect + github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect + github.com/containerd/ttrpc v1.2.2 // indirect + github.com/containerd/typeurl/v2 v2.1.1 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/docker/cli v24.0.6+incompatible // indirect - github.com/docker/distribution v2.8.2+incompatible // indirect - github.com/docker/docker v24.0.7+incompatible // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/docker/cli v25.0.1+incompatible // indirect + github.com/docker/distribution v2.8.3+incompatible // indirect + github.com/docker/docker v26.1.4+incompatible // indirect github.com/docker/docker-credential-helpers v0.7.0 // indirect - github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-connections v0.5.0 // indirect + github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/emicklei/go-restful/v3 v3.10.1 // indirect - github.com/evanphx/json-patch v5.6.0+incompatible // indirect + github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/edsrzf/mmap-go v1.1.0 // indirect + github.com/elliotchance/phpserialize v1.4.0 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/evanphx/json-patch v5.7.0+incompatible // indirect github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect + github.com/facebookincubator/nvdtools v0.1.5 // indirect github.com/fatih/camelcase v1.0.0 // indirect + github.com/felixge/fgprof v0.9.3 // indirect + github.com/felixge/httpsnoop v1.0.3 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gdamore/encoding v1.0.0 // indirect - github.com/go-errors/errors v1.4.2 // indirect + github.com/github/go-spdx/v2 v2.2.0 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect + github.com/glebarez/sqlite v1.11.0 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect + github.com/go-git/go-git/v5 v5.12.0 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect - github.com/go-logr/logr v1.3.0 // indirect + github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect + github.com/go-restruct/restruct v1.2.0-alpha // indirect + github.com/go-test/deep v1.1.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/protobuf v1.5.3 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/go-containerregistry v0.19.1 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/google/licensecheck v0.3.1 // indirect + github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 // indirect + github.com/google/s2a-go v0.1.7 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect - github.com/google/uuid v1.3.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.0 // indirect + github.com/gookit/color v1.5.4 // indirect github.com/gorilla/mux v1.8.0 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/gosuri/uitable v0.0.4 // indirect github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect + github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect + github.com/hashicorp/go-getter v1.7.4 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect + github.com/hashicorp/go-safetemp v1.0.0 // indirect + github.com/hashicorp/go-version v1.6.0 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect github.com/huandu/xstrings v1.4.0 // indirect - github.com/imdario/mergo v0.3.13 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect + github.com/imdario/mergo v0.3.15 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jinzhu/copier v0.4.0 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmoiron/sqlx v1.3.5 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/compress v1.16.0 // indirect + github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/compress v1.17.4 // indirect + github.com/klauspost/pgzip v1.2.5 // indirect + github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f // indirect + github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d // indirect + github.com/knqyf263/go-rpmdb v0.1.1 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/masahiro331/go-mvn-version v0.0.0-20210429150710-d3157d602a08 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect + github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect + github.com/mholt/archiver/v3 v3.5.1 // indirect + github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/locker v1.0.1 // indirect github.com/moby/spdystream v0.2.0 // indirect + github.com/moby/sys/mountinfo v0.7.1 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/signal v0.7.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect - github.com/morikuni/aec v1.0.0 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/nwaples/rardecode v1.1.0 // indirect github.com/onsi/ginkgo v1.16.5 // indirect - github.com/onsi/gomega v1.27.6 // indirect + github.com/onsi/gomega v1.31.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.1.0-rc5 // indirect + github.com/opencontainers/image-spec v1.1.0-rc6 // indirect + github.com/opencontainers/runtime-spec v1.1.0 // indirect + github.com/opencontainers/selinux v1.11.0 // indirect + github.com/openvex/go-vex v0.2.5 // indirect + github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554 // indirect + github.com/package-url/packageurl-go v0.1.1 // indirect + github.com/pborman/indent v1.2.1 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pierrec/lz4/v4 v4.1.15 // indirect + github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pkg/profile v1.7.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.16.0 // indirect github.com/prometheus/client_model v0.4.0 // indirect github.com/prometheus/common v0.44.0 // indirect github.com/prometheus/procfs v0.10.1 // indirect - github.com/rivo/uniseg v0.4.3 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rm-hull/colorjson v0.0.0-20220923220430-b50ee91dc6f6 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rubenv/sql-migrate v1.5.2 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/saferwall/pe v1.5.2 // indirect + github.com/sagikazarmark/locafero v0.3.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect + github.com/sassoftware/go-rpmutils v0.3.0 // indirect + github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/sirupsen/logrus v1.9.3 // indirect - github.com/spf13/cast v1.5.0 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spdx/tools-golang v0.5.3 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.5.1 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.17.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/sylabs/sif/v2 v2.11.5 // indirect + github.com/sylabs/squashfs v0.6.1 // indirect + github.com/therootcompany/xz v1.0.1 // indirect + github.com/ulikunitz/xz v0.5.11 // indirect + github.com/vbatts/go-mtree v0.5.3 // indirect + github.com/vbatts/tar-split v0.11.3 // indirect + github.com/vifraa/gopom v1.0.0 // indirect + github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 // indirect + github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b // indirect + github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect - github.com/xeipuuv/gojsonschema v1.2.0 // indirect + github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xlab/treeprint v1.2.0 // indirect - go.opentelemetry.io/otel v1.14.0 // indirect - go.opentelemetry.io/otel/trace v1.14.0 // indirect + github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect + github.com/zclconf/go-cty v1.14.0 // indirect + go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect + go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 // indirect + go.opentelemetry.io/otel v1.19.0 // indirect + go.opentelemetry.io/otel/metric v1.19.0 // indirect + go.opentelemetry.io/otel/trace v1.19.0 // indirect go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect - golang.org/x/crypto v0.14.0 // indirect - golang.org/x/net v0.17.0 // indirect - golang.org/x/oauth2 v0.8.0 // indirect - golang.org/x/sync v0.3.0 // indirect - golang.org/x/sys v0.14.0 // indirect - golang.org/x/term v0.13.0 // indirect - golang.org/x/time v0.3.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 // indirect - google.golang.org/grpc v1.56.3 // indirect - google.golang.org/protobuf v1.30.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.22.0 // indirect + golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/oauth2 v0.19.0 // indirect + golang.org/x/sync v0.6.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/term v0.19.0 // indirect + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.18.0 // indirect + golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + google.golang.org/api v0.152.0 // indirect + google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect + google.golang.org/grpc v1.59.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apiserver v0.28.3 // indirect - k8s.io/component-base v0.28.3 // indirect - k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect - k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect - oras.land/oras-go v1.2.4 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gorm.io/gorm v1.25.9 // indirect + k8s.io/apiserver v0.30.1 // indirect + k8s.io/component-base v0.30.1 // indirect + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + modernc.org/libc v1.41.0 // indirect + modernc.org/mathutil v1.6.0 // indirect + modernc.org/memory v1.7.2 // indirect + modernc.org/sqlite v1.29.6 // indirect + oras.land/oras-go v1.2.5 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) diff --git a/go.sum b/go.sum index 4a42b0625c..b74e406280 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,219 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= +cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= +cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= +cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= +cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= +cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= +cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= +cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.98.0/go.mod h1:ua6Ush4NALrHk5QXDWnjvZHN93OuF0HfuEPq9I1X0cM= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= +cloud.google.com/go v0.110.10 h1:LXy9GEO+timppncPIAZoOj3l58LIU9k+kn48AN7IO3Y= +cloud.google.com/go v0.110.10/go.mod h1:v1OoFqYxiBkUrruItNM3eT4lLByNjxmJSV/xDKJNnic= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= +cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= +cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= +cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= +cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= +cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= +cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/storage v1.35.1 h1:B59ahL//eDfx2IIKFBeT5Atm9wnNmj3+8xG/W4WB//w= +cloud.google.com/go/storage v1.35.1/go.mod h1:M6M/3V/D3KpzMTJyPOR/HU6n2Si5QdaXYEsng2xgOs8= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 h1:59MxjQVfjXsBpLy+dbd2/ELV5ofnUkUZBvWSC85sheA= +github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= -github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/CycloneDX/cyclonedx-go v0.8.0 h1:FyWVj6x6hoJrui5uRQdYZcSievw3Z32Z88uYzG/0D6M= +github.com/CycloneDX/cyclonedx-go v0.8.0/go.mod h1:K2bA+324+Og0X84fA8HhN2X066K7Bxz4rpMQ4ZhjtSk= +github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= +github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= +github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= +github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= +github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= @@ -19,28 +221,92 @@ github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= +github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/Microsoft/hcsshim v0.11.0 h1:7EFNIY4igHEXUdj1zXgAyU3fLc7QfOKHbkldRVTBdiM= -github.com/Microsoft/hcsshim v0.11.0/go.mod h1:OEthFdQv/AD2RAdzR6Mm1N1KPCztGKDurW1Z8b8VGMM= +github.com/Microsoft/hcsshim v0.11.4 h1:68vKo2VN8DE9AdN4tnkWnmdhqdbpUFM8OF3Airm7fz8= +github.com/Microsoft/hcsshim v0.11.4/go.mod h1:smjE4dvqPX9Zldna+t5FG3rnoHhaB7QYxPRqGcpAD9w= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/acobaugh/osrelease v0.1.0 h1:Yb59HQDGGNhCj4suHaFQQfBps5wyoKLSSX/J/+UifRE= +github.com/acobaugh/osrelease v0.1.0/go.mod h1:4bFEs0MtgHNHBrmHCt67gNisnabCRAlzdVasCEGHTWY= github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/anchore/clio v0.0.0-20240209204744-cb94e40a4f65 h1:u9XrEabKlGPsrmRvAER+kUKkwXiJfLyqGhmOTFsXjX4= +github.com/anchore/clio v0.0.0-20240209204744-cb94e40a4f65/go.mod h1:8Jr7CjmwFVcBPtkJdTpaAGHimoGJGfbExypjzOu87Og= +github.com/anchore/fangs v0.0.0-20231201140849-5075d28d6d8b h1:L/djgY7ZbZ/38+wUtdkk398W3PIBJLkt1N8nU/7e47A= +github.com/anchore/fangs v0.0.0-20231201140849-5075d28d6d8b/go.mod h1:TLcE0RE5+8oIx2/NPWem/dq1DeaMoC+fPEH7hoSzPLo= +github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537 h1:GjNGuwK5jWjJMyVppBjYS54eOiiSNv4Ba869k4wh72Q= +github.com/anchore/go-collections v0.0.0-20240216171411-9321230ce537/go.mod h1:1aiktV46ATCkuVg0O573ZrH56BUawTECPETbZyBcqT8= +github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a h1:nJ2G8zWKASyVClGVgG7sfM5mwoZlZ2zYpIzN2OhjWkw= +github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a/go.mod h1:ubLFmlsv8/DFUQrZwY5syT5/8Er3ugSr4rDFwHsE3hg= +github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU= +github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb/go.mod h1:DmTY2Mfcv38hsHbG78xMiTDdxFtkHpgYNVDPsF2TgHk= +github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc= +github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= +github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0vW0nnNKJfJieyH/TZ9UYAnTZs5/gHTdAe8= +github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04/go.mod h1:6dK64g27Qi1qGQZ67gFmBFvEHScy0/C8qhQhNe5B5pQ= +github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 h1:rmZG77uXgE+o2gozGEBoUMpX27lsku+xrMwlmBZJtbg= +github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= +github.com/anchore/grype v0.77.0 h1:HoTdZ67INrEpEiSKL713zY+j77HxoEAcsMPIZDZ4yP4= +github.com/anchore/grype v0.77.0/go.mod h1:k6QLcebOqPm+90y8mMesOJM6A6DYQllOic6Tmz507sc= +github.com/anchore/packageurl-go v0.1.1-0.20240312213626-055233e539b4 h1:SjemQ90fgflz39HG+VMkNfrpUVJpcFW6ZFA3TDXqzBM= +github.com/anchore/packageurl-go v0.1.1-0.20240312213626-055233e539b4/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4= +github.com/anchore/stereoscope v0.0.2-0.20240229175558-fe426d1b1c84 h1:/E74wU51M87fX5UWHubLZiENXbuAci+xtbSb+JFsIYg= +github.com/anchore/stereoscope v0.0.2-0.20240229175558-fe426d1b1c84/go.mod h1:evQiJMQG56Z7/L5uhA8kfhhjF6ESJUZzUH9ms6bQ2Co= +github.com/anchore/syft v1.2.0 h1:e6cJVzHErrZuYTWlSjxI/JbXS5ipaN8cdjXwGpd34MQ= +github.com/anchore/syft v1.2.0/go.mod h1:0oY5LHY9MC/Mui6ZTjd0jcJRU6U6HNxaoQPWbZ4RhhY= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= +github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= +github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 h1:vmXNl+HDfqqXgr0uY1UgK1GAhps8nbAAtqHNBcgyf+4= +github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46/go.mod h1:olhPNdiiAAMiSujemd1O/sc6GcyePr23f/6uGKtthNg= +github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 h1:rcEG5HI490FF0a7zuvxOxen52ddygCfNVjP0XOCMl+M= +github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492/go.mod h1:9Beu8XsUNNfzml7WBf3QmyPToP1wm1Gj/Vc5UJKqTzU= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535 h1:4daAzAu0S6Vi7/lbWECcX0j45yZReDZ56BQsrVBOEEY= github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= -github.com/aws/aws-sdk-go v1.38.49 h1:E31vxjCe6a5I+mJLmUGaZobiWmg9KdWaud9IfceYeYQ= -github.com/aws/aws-sdk-go v1.38.49/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.288 h1:Ln7fIao/nl0ACtelgR1I4AiEw/GLNkKcXfCaHupUW5Q= +github.com/aws/aws-sdk-go v1.44.288/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA= +github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= +github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bmatcuk/doublestar/v2 v2.0.4 h1:6I6oUiT/sU27eE2OFcWqBhL1SwjyvQuOssxT4a1yidI= +github.com/bmatcuk/doublestar/v2 v2.0.4/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQmTZRptLie8RgRw= +github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/bshuster-repo/logrus-logstash-hook v1.0.0 h1:e+C0SB5R1pu//O4MQ3f9cFuPGoOVeF2fE4Og9otCc70= github.com/bshuster-repo/logrus-logstash-hook v1.0.0/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd h1:rFt+Y/IK1aEZkEHchZRSq9OQbsSzIT/OrI8YFFmRIng= @@ -49,24 +315,68 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembj github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/charmbracelet/bubbles v0.18.0 h1:PYv1A036luoBGroX6VWjQIE9Syf2Wby2oOl/39KLfy0= +github.com/charmbracelet/bubbles v0.18.0/go.mod h1:08qhZhtIwzgrtBjAcJnij1t1H0ZRjwHyGsy6AL11PSw= +github.com/charmbracelet/bubbletea v0.25.0 h1:bAfwk7jRz7FKFl9RzlIULPkStffg5k6pNt5dywy4TcM= +github.com/charmbracelet/bubbletea v0.25.0/go.mod h1:EN3QDR1T5ZdWmdfDzYcqOCAps45+QIJbLOBxmVNWNNg= +github.com/charmbracelet/lipgloss v0.10.0 h1:KWeXFSexGcfahHX+54URiZGkBFazf70JNMtwg/AFW3s= +github.com/charmbracelet/lipgloss v0.10.0/go.mod h1:Wig9DSfvANsxqkRsqj6x87irdy123SR4dOXlKa91ciE= +github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= +github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= +github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= +github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= -github.com/containerd/containerd v1.7.6 h1:oNAVsnhPoy4BTPQivLgTzI9Oleml9l/+eYIDYXRCYo8= -github.com/containerd/containerd v1.7.6/go.mod h1:SY6lrkkuJT40BVNO37tlYTSnKJnP5AXBc0fhx0q+TJ4= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= +github.com/containerd/containerd v1.7.12 h1:+KQsnv4VnzyxWcfO9mlxxELaoztsDEjOuCMPAuPqgU0= +github.com/containerd/containerd v1.7.12/go.mod h1:/5OMpE1p0ylxtEUGY8kuCYkDRzJm9NO1TFMWjUpdevk= github.com/containerd/continuity v0.4.2 h1:v3y/4Yz5jwnvqPKJJ+7Wf93fyWoCB3F5EclWG023MDM= github.com/containerd/continuity v0.4.2/go.mod h1:F6PTNCKepoxEaXLQp3wDAjygEnImnZ/7o4JzpodfroQ= +github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= +github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= +github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= +github.com/containerd/ttrpc v1.2.2 h1:9vqZr0pxwOF5koz6N0N3kJ0zDHokrcPxIR/ZR2YFtOs= +github.com/containerd/ttrpc v1.2.2/go.mod h1:sIT6l32Ph/H9cvnJsfXM5drIVzTr5A2flTf1G5tYZak= +github.com/containerd/typeurl/v2 v2.1.1 h1:3Q4Pt7i8nYwy2KmQWIw2+1hTvwTE/6w9FqcttATPO/4= +github.com/containerd/typeurl/v2 v2.1.1/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= @@ -74,26 +384,32 @@ github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/derailed/popeye v0.11.1 h1:bjt5mXkcXY696ipuJqwY1sa5s3i431L9BlkQc6EuaqE= -github.com/derailed/popeye v0.11.1/go.mod h1:NkvjHH1F94tE7Ui17PlYiagQcFt7yXUV2hIhPzSK+0w= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da h1:ZOjWpVsFZ06eIhnh4mkaceTiVoktdU67+M7KDHJ268M= +github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da/go.mod h1:B3tI9iGHi4imdLi4Asdha1Sc6feLMTfPLXh9IUYmysk= +github.com/derailed/popeye v0.11.3 h1:gQUp6zuSIRDBdyLS1Ln0nFs8FbQ+KGE+iQxe0w4Ug8M= +github.com/derailed/popeye v0.11.3/go.mod h1:HygqX7A8BwidorJjJUnWDZ5AvbxHIU7uRwXgOtn9GwY= github.com/derailed/tcell/v2 v2.3.1-rc.3 h1:9s1fmyRcSPRlwr/C9tcpJKCujbrtmPpST6dcMUD2piY= github.com/derailed/tcell/v2 v2.3.1-rc.3/go.mod h1:nf68BEL8fjmXQHJT3xZjoZFs2uXOzyJcNAQqGUEMrFY= -github.com/derailed/tview v0.8.1 h1:hvNR3LLrWEuaQbPYfBnRn7bYkxCP26K6nX9J+MGlhyw= -github.com/derailed/tview v0.8.1/go.mod h1:q+odnnhO6QDPpBT+0dqaWj+X+uoJ6MJehXj9shgP+Cw= +github.com/derailed/tview v0.8.3 h1:jhN7LW7pfCWf7Z6VC5Dpi/1usavOBZxz2mY90//TMsU= +github.com/derailed/tview v0.8.3/go.mod h1:q+odnnhO6QDPpBT+0dqaWj+X+uoJ6MJehXj9shgP+Cw= +github.com/dgrijalva/jwt-go/v4 v4.0.0-preview1/go.mod h1:+hnT3ywWDTAFrW5aE+u2Sa/wT555ZqwoCS+pk3p6ry4= github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 h1:aBfCb7iqHmDEIp6fBvC/hQUddQfg+3qdYjwzaiP9Hnc= github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2/go.mod h1:WHNsWjnIn2V1LYOrME7e8KxSeKunYHsxEm4am0BUtcI= -github.com/docker/cli v24.0.6+incompatible h1:fF+XCQCgJjjQNIMjzaSmiKJSCcfcXb3TWTcc7GAneOY= -github.com/docker/cli v24.0.6+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= -github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v24.0.7+incompatible h1:Wo6l37AuwP3JaMnZa226lzVXGA3F9Ig1seQen0cKYlM= -github.com/docker/docker v24.0.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/cli v25.0.1+incompatible h1:mFpqnrS6Hsm3v1k7Wa/BO23oz0k121MTbTO1lpcGSkU= +github.com/docker/cli v25.0.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= +github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/docker v26.1.4+incompatible h1:vuTpXDuoga+Z38m1OZHzl7NKisKWaWlhjQk7IDPSLsU= +github.com/docker/docker v26.1.4+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= -github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= -github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= @@ -102,43 +418,105 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4 github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1 h1:ZClxb8laGDf5arXfYcAtECDFgAgHklGI8CxgjHnXKJ4= github.com/docker/libtrust v0.0.0-20150114040149-fa567046d9b1/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= -github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ= -github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= +github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= +github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= +github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elliotchance/phpserialize v1.4.0 h1:cAp/9+KSnEbUC8oYCE32n2n84BeW8HOY3HMDI8hG2OY= +github.com/elliotchance/phpserialize v1.4.0/go.mod h1:gt7XX9+ETUcLXbtTKEuyrqW3lcLUAeS/AnGZ2e49TZs= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= +github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= +github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= +github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.1/go.mod h1:AY7fTTXNdv/aJ2O5jwpxAPOWUZ7hQAEvzN5Pf27BkQQ= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= -github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E9/baC+qXE/TeeyBRzgJDws= +github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ2tG6yudJd8LBksgI= +github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d h1:105gxyaGwCFad8crR9dcMQWvV9Hvulu6hwUh4tWPJnM= github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d/go.mod h1:ZZMPRZwes7CROmyNKgQzC3XPs6L/G2EJLHddWejkmf4= +github.com/facebookincubator/flog v0.0.0-20190930132826-d2511d0ce33c/go.mod h1:QGzNH9ujQ2ZUr/CjDGZGWeDAVStrWNjHeEcjJL96Nuk= +github.com/facebookincubator/nvdtools v0.1.5 h1:jbmDT1nd6+k+rlvKhnkgMokrCAzHoASWE5LtHbX2qFQ= +github.com/facebookincubator/nvdtools v0.1.5/go.mod h1:Kh55SAWnjckS96TBSrXI99KrEKH4iB0OJby3N8GRJO4= github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= +github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= +github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/foxcpp/go-mockdns v1.0.0 h1:7jBqxd3WDWwi/6WhDvacvH1XsN3rOLXyHM1uhvIx6FI= github.com/foxcpp/go-mockdns v1.0.0/go.mod h1:lgRN6+KxQBawyIghpnl5CezHFGS9VLzvtVlwxvzXTQ4= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fvbommel/sortorder v1.1.0 h1:fUmoe+HLsBTctBDoaBwpQo5N+nrCp8g/BjKb/6ZQmYw= github.com/fvbommel/sortorder v1.1.0/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/github/go-spdx/v2 v2.2.0 h1:yBBLMasHA70Ujd35OpL/OjJOWWVNXcJGbars0GinGRI= +github.com/github/go-spdx/v2 v2.2.0/go.mod h1:hMCrsFgT0QnCwn7G8gxy/MxMpy67WgZrwFeISTn0o6w= +github.com/gkampitakis/ciinfo v0.3.0 h1:gWZlOC2+RYYttL0hBqcoQhM7h1qNkVqvRCV1fOvpAv8= +github.com/gkampitakis/ciinfo v0.3.0/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.3 h1:2cJnBgHzJhh0Jk5XBIyDYDe1Ylfncoa9r9bVJ5qvOAE= +github.com/gkampitakis/go-snaps v0.5.3/go.mod h1:ZABkO14uCuVxBHAXAfKG+bqNz+aa1bGPAg8jkI0Nk8Y= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw= +github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= -github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= @@ -147,12 +525,16 @@ github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2Kv github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc= +github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= +github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= +github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU= github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= github.com/gobuffalo/packd v1.0.1 h1:U2wXfRr4E9DH8IdsDLlRFwTZTK7hLfq9qT/QHXGVe/0= @@ -166,12 +548,26 @@ github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7a github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= @@ -179,11 +575,20 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.2/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= github.com/gomodule/redigo v1.8.2/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= @@ -192,46 +597,164 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-containerregistry v0.19.1 h1:yMQ62Al6/V0Z7CqIrrS1iYoA5/oQCm88DeNujc7C1KY= +github.com/google/go-containerregistry v0.19.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/licensecheck v0.3.1 h1:QoxgoDkaeC4nFrtGN1jV7IPmDCHFNIVh54e5hSt6sPs= +github.com/google/licensecheck v0.3.1/go.mod h1:ORkR35t/JjW+emNKtfJDII0zlciG9JgbT7SmsohlHmY= +github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= +github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= +github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gosuri/uitable v0.0.4 h1:IG2xLKRvErL3uhY6e1BylFzG+aJiwQviDDTfOKeKTpY= github.com/gosuri/uitable v0.0.4/go.mod h1:tKR86bXuXPZazfOTG1FIzvjIdXzd0mo4Vtn16vt0PJo= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 h1:pdN6V1QBWetyv/0+wjACpqVH+eVULgEjkurDLq3goeM= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4= +github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0= +github.com/hashicorp/consul/api v1.11.0/go.mod h1:XjsvQN+RJGWI2TWy1/kqaE16HrR2J/FWgkYjdZQsX9M= +github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-getter v1.7.4 h1:3yQjWuxICvSpYwqSayAdKRFcvBl1y/vogCxczWSmix0= +github.com/hashicorp/go-getter v1.7.4/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744= +github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= +github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= +github.com/hashicorp/go-safetemp v1.0.0 h1:2HR189eFNrjHQyENnQMMpCiBAsRxzbTMIgBhEyExpmo= +github.com/hashicorp/go-safetemp v1.0.0/go.mod h1:oaerMy3BhqiTbVye6QuFhFtIceqFoDHxNAB65b+Rj1I= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.1/go.mod h1:4gW7WsVCke5TE7EPeYliwHlRUyBtfCwuFwuMg2DmyNY= +github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.2.2/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= +github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= +github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -242,17 +765,40 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/karrick/godirwalk v1.17.0 h1:b4kY7nqDdioR/6qnbHQyDvmA17u5G1cZ6J+CZXwSWoI= -github.com/karrick/godirwalk v1.17.0/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= +github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 h1:WdAeg/imY2JFPc/9CST4bZ80nNJbiBFCAdSZCSgrS5Y= +github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953/go.mod h1:6o+UrvuZWc4UTyBhQf0LGjW9Ld7qJxLz/OqvSOWWlEc= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4= -github.com/klauspost/compress v1.16.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= +github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= +github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= +github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= +github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= +github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE= +github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f h1:GvCU5GXhHq+7LeOzx/haG7HSIZokl3/0GkoUFzsRJjg= +github.com/knqyf263/go-apk-version v0.0.0-20200609155635-041fdbb8563f/go.mod h1:q59u9px8b7UTj0nIjEjvmTWekazka6xIt6Uogz5Dm+8= +github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d h1:X4cedH4Kn3JPupAwwWuo4AzYp16P0OyLO9d7OnMZc/c= +github.com/knqyf263/go-deb-version v0.0.0-20190517075300-09fca494f03d/go.mod h1:o8sgWoz3JADecfc/cTYD92/Et1yMqMy0utV1z+VaZao= +github.com/knqyf263/go-rpmdb v0.1.1 h1:oh68mTCvp1XzxdU7EfafcWzzfstUZAEa3MW0IJye584= +github.com/knqyf263/go-rpmdb v0.1.1/go.mod h1:9LQcoMCMQ9vrF7HcDtXfvqGO4+ddxFQ8+YF/0CVGDww= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -271,10 +817,14 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs= +github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magiconair/properties v1.8.5 h1:b6kJs+EmPFMYGkow9GiUyCyOvIwYetYJ3fSaWak/Gls= +github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= @@ -283,36 +833,86 @@ github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/masahiro331/go-mvn-version v0.0.0-20210429150710-d3157d602a08 h1:AevUBW4cc99rAF8q8vmddIP8qd/0J5s/UyltGbp66dg= +github.com/masahiro331/go-mvn-version v0.0.0-20210429150710-d3157d602a08/go.mod h1:JOkBRrE1HvgTyjk6diFtNGgr8XJMtIfiBzkL5krqzVk= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw= +github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI= -github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/miekg/dns v1.1.25 h1:dFwPR6SfLtrSwgDcIq2bcU/gVutB4sNApq2HBdqcakg= -github.com/miekg/dns v1.1.25/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= +github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= +github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= +github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5 h1:tQRHcLQwnwrPq2j2Qra/NnyjyESBGwdeBeVdAE9kXYg= +github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5/go.mod h1:vYT9HE7WCvL64iVeZylKmCsWKfE+JZ8105iuh2Trk8g= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.41 h1:WMszZWJG0XmzbK9FEmzH2TVcqYzFesusSIB41b8KHxY= +github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= +github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= -github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/mountinfo v0.7.1 h1:/tTvQaSJRr2FshkhXiIpux6fQ2Zvc4j7tAhMTStAG2g= +github.com/moby/sys/mountinfo v0.7.1/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI= +github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -326,105 +926,254 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA= +github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/nwaples/rardecode v1.1.0 h1:vSxaY8vQhOcVr4mm5e8XllHWTiM4JF507A0Katqw7MQ= +github.com/nwaples/rardecode v1.1.0/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.9.4 h1:xR7vG4IXt5RWx6FfIjyAtsoMAtnc3C/rFXBBd2AjZwE= -github.com/onsi/ginkgo/v2 v2.9.4/go.mod h1:gCQYp2Q+kSoIj7ykSVb9nskRSsR6PUj4AiLywzIhbKM= +github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= +github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= -github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE= -github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg= +github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE= +github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.1.0-rc5 h1:Ygwkfw9bpDvs+c9E34SdgGOj41dX/cbdlwvlWt0pnFI= -github.com/opencontainers/image-spec v1.1.0-rc5/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/opencontainers/image-spec v1.1.0-rc6 h1:XDqvyKsJEbRtATzkgItUqBA7QHk58yxX1Ov9HERHNqU= +github.com/opencontainers/image-spec v1.1.0-rc6/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/opencontainers/runtime-spec v1.1.0 h1:HHUyrt9mwHUjtasSbXSMvs4cyFxh+Bll4AjJ9odEGpg= +github.com/opencontainers/runtime-spec v1.1.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= +github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= +github.com/openvex/go-vex v0.2.5 h1:41utdp2rHgAGCsG+UbjmfMG5CWQxs15nGqir1eRgSrQ= +github.com/openvex/go-vex v0.2.5/go.mod h1:j+oadBxSUELkrKh4NfNb+BPo77U3q7gdKME88IO/0Wo= +github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554 h1:FvA4bwjKpPqik5WsQ8+4z4DKWgA1tO1RTTtNKr5oYNA= +github.com/owenrumney/go-sarif v1.1.2-0.20231003122901-1000f5e05554/go.mod h1:n73K/hcuJ50MiVznXyN4rde6fZY7naGKWBXOLFTyc94= +github.com/package-url/packageurl-go v0.1.1 h1:KTRE0bK3sKbFKAk3yy63DpeskU7Cvs/x/Da5l+RtzyU= +github.com/package-url/packageurl-go v0.1.1/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM= +github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrpz5K6Vw= +github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/petergtz/pegomock v2.9.0+incompatible h1:BKfb5XfkJfehe5T+O1xD4Zm26Sb9dnRj7tHxLYwUPiI= github.com/petergtz/pegomock v2.9.0+incompatible/go.mod h1:nuBLWZpVyv/fLo56qTwt/AUau7jgouO1h7bEvZCq82o= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5 h1:Ii+DKncOVM8Cu1Hc+ETb5K+23HdAMvESYE3ZJ5b5cMI= github.com/phayes/freeport v0.0.0-20220201140144-74d24b5ae9f5/go.mod h1:iIss55rKnNBTvrwdmkUpLnDpZoAHvWaiq5+iMmen4AE= +github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= +github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= +github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= +github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= +github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.16.0 h1:yk/hx9hDbrGHovbci4BY+pRMfSuuat626eFsHb7tmT8= github.com/prometheus/client_golang v1.16.0/go.mod h1:Zsulrv/L9oM40tJ7T815tM89lFEugiJ9HzIqaAx4LKc= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= +github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/rakyll/hey v0.1.4 h1:hhc8GIqHN4+rPFZvkM9lkCQGi7da0sINM83xxpFkbPA= github.com/rakyll/hey v0.1.4/go.mod h1:nAOTOo+L52KB9SZq/M6J18kxjto4yVtXQDjU2HgjUPI= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= -github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rm-hull/colorjson v0.0.0-20220923220430-b50ee91dc6f6 h1:avrA8y9AJF9WtGipEvrM8I/7XoKcxEk30659rPHJlnM= github.com/rm-hull/colorjson v0.0.0-20220923220430-b50ee91dc6f6/go.mod h1:tJTNxpJk1e15vd8WY5lsj9Tq5vjdnNz3YAbCwxYskBs= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= -github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= +github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/rubenv/sql-migrate v1.5.2 h1:bMDqOnrJVV/6JQgQ/MxOpU+AdO8uzYYA/TxFUBzFtS0= github.com/rubenv/sql-migrate v1.5.2/go.mod h1:H38GW8Vqf8F0Su5XignRyaRcbXbJunSWxs+kmzlg0Is= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sahilm/fuzzy v0.1.0 h1:FzWGaw2Opqyu+794ZQ9SYifWv2EIXpwP4q8dY1kDAwI= -github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/saferwall/pe v1.5.2 h1:h5lLtLsyxGHQ9dN6cd8EfeLEBEo5gdqJpkuw4o4vTMY= +github.com/saferwall/pe v1.5.2/go.mod h1:SNzv3cdgk8SBI0UwHfyTcdjawfdnN+nbydnEL7GZ25s= +github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= +github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ= +github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= +github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= +github.com/sanity-io/litter v1.5.5 h1:iE+sBxPBzoK6uaEP5Lt3fHNgpKcHXc/A2HGETy0uJQo= +github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= +github.com/sassoftware/go-rpmutils v0.3.0 h1:tE4TZ8KcOXay5iIP64P291s6Qxd9MQCYhI7DU+f3gFA= +github.com/sassoftware/go-rpmutils v0.3.0/go.mod h1:hM9wdxFsjUFR/tJ6SMsLrJuChcucCa0DsCzE9RMfwMo= +github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e h1:7q6NSFZDeGfvvtIRwBrU/aegEYJYmvev0cHAwo17zZQ= +github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= +github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= +github.com/spdx/tools-golang v0.5.3 h1:ialnHeEYUC4+hkm5vJm4qz2x+oEJbS0mAMFrNXdQraY= +github.com/spdx/tools-golang v0.5.3/go.mod h1:/ETOahiAo96Ob0/RAIBmFZw6XN0yTnyr/uFZm2NTMhI= +github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= +github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/cobra v1.3.0/go.mod h1:BrRVncBjOJa/eUcVVm9CE+oC6as8k+VYr4NY7WCi9V4= github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.10.0/go.mod h1:SoyBPwAtKDzypXNDFKN5kzH7ppppbGZtls1UpIy5AsM= +github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= +github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/sylabs/sif/v2 v2.11.5 h1:7ssPH3epSonsTrzbS1YxeJ9KuqAN7ISlSM61a7j/mQM= +github.com/sylabs/sif/v2 v2.11.5/go.mod h1:GBoZs9LU3e4yJH1dcZ3Akf/jsqYgy5SeguJQC+zd75Y= +github.com/sylabs/squashfs v0.6.1 h1:4hgvHnD9JGlYWwT0bPYNt9zaz23mAV3Js+VEgQoRGYQ= +github.com/sylabs/squashfs v0.6.1/go.mod h1:ZwpbPCj0ocIvMy2br6KZmix6Gzh6fsGQcCnydMF+Kx8= +github.com/terminalstatic/go-xsd-validate v0.1.5 h1:RqpJnf6HGE2CB/lZB1A8BYguk8uRtcvYAPLCF15qguo= +github.com/terminalstatic/go-xsd-validate v0.1.5/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw= +github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= +github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/tidwall/gjson v1.17.0 h1:/Jocvlh98kcTfpN2+JzGQWQcqrPQwDrVEMApx/M5ZwM= +github.com/tidwall/gjson v1.17.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= +github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= +github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +github.com/vbatts/go-mtree v0.5.3 h1:S/jYlfG8rZ+a0bhZd+RANXejy7M4Js8fq9U+XoWTd5w= +github.com/vbatts/go-mtree v0.5.3/go.mod h1:eXsdoPMdL2jcJx6HweWi9lYQxBsTp4lNhqqAjgkZUg8= +github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= +github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= +github.com/vifraa/gopom v1.0.0 h1:L9XlKbyvid8PAIK8nr0lihMApJQg/12OBvMA28BcWh0= +github.com/vifraa/gopom v1.0.0/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 h1:jIVmlAFIqV3d+DOxazTR9v+zgj8+VYuQBzPgBZvWBHA= +github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651/go.mod h1:b26F2tHLqaoRQf8DywqzVaV1MQ9yvjb0OMcNl7Nxu20= +github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b h1:uWNQ0khA6RdFzODOMwKo9XXu7fuewnnkHykUtuKru8s= +github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b/go.mod h1:ewlIKbKV8l+jCj8rkdXIs361ocR5x3qGyoCSca47Gx8= +github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0 h1:0KGbf+0SMg+UFy4e1A/CPVvXn21f1qtWdeJwxZFoQG8= +github.com/wagoodman/go-progress v0.0.0-20230925121702-07e42b3cdba0/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA= +github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= @@ -432,10 +1181,17 @@ github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHo github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= +github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= +github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= @@ -443,134 +1199,619 @@ github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMzt github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50/go.mod h1:NUSPSUX/bi6SeDMUh6brw0nXpxHnc96TguQh0+r/ssA= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f h1:ERexzlUfuTvpE74urLSbIQW0Z/6hF9t8U4NsJLaioAY= github.com/yvasiyarov/newrelic_platform_go v0.0.0-20140908184405-b21fdbd4370f/go.mod h1:GlGEuHIJweS1mbCqG+7vt2nvWLzLLnRHbXz5JKd/Qbg= +github.com/zclconf/go-cty v1.14.0 h1:/Xrd39K7DXbHzlisFP9c4pHao4yyf+/Ug9LEz+Y/yhc= +github.com/zclconf/go-cty v1.14.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= +go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= +go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= +go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM= -go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= -go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M= -go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0 h1:x8Z78aZx8cOF0+Kkazoc7lwUNMGy0LrzEMxTm4BbTxg= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0/go.mod h1:62CPTSry9QZtOaSsE3tOzhx6LzDhHnXJ6xHeMNNiM6Q= +go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= +go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= +go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= +go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= +go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= +go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= +go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= +go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= +go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= +golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= +golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w= +golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= -golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181017193950-04a2e542c03f/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= +golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= -golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A= +golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= +golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200124204421-9fbb57f87de9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q= -golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.8.0 h1:vSDcovVPld282ceKgDimkRSC8kpaH1dgyc9UMzlt84Y= -golang.org/x/tools v0.8.0/go.mod h1:JxBZ99ISMI5ViVkT1tr6tdNmXeTrcpVSD3vZ1RsRdN4= +golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= +golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= +google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= +google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= +google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= +google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= +google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.62.0/go.mod h1:dKmwPCydfsad4qCH08MSdgWjfHOyfpd4VtDGgRFdavw= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= +google.golang.org/api v0.152.0 h1:t0r1vPnfMc260S2Ci+en7kfCZaLOPs5KI0sVV/6jZrY= +google.golang.org/api v0.152.0/go.mod h1:3qNJX5eOmhiWYc67jRA/3GsDw97UFb5ivv7Y2PrriAY= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19 h1:0nDDozoAU19Qb2HwhXadU8OcsiO/09cnTqhUtq2MEOM= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230525234030-28d5490b6b19/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= +google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= +google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= +google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= +google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= +google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211008145708-270636b82663/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211028162531-8db9c33dc351/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211129164237-f09f9a12af12/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211203200212-54befc351ae9/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 h1:wpZ8pe2x1Q3f2KyT5f8oP/fa9rHAKgFPr/HZdNuS+PQ= +google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:J7XzRzVy1+IPwWHZUzoD0IccYZIrXILAQpc+Qy9CMhY= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17 h1:JpwMPBpFN3uKhdaekDpiNlImDdkUAyiJ6ez/uxGaUSo= +google.golang.org/genproto/googleapis/api v0.0.0-20231106174013-bbf56f31fb17/go.mod h1:0xJLfVdJqpAPl8tDg1ujOCGzx6LFLttXT5NhllGOXY4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= -google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= +google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= +google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= +google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= +google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= +google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.59.0 h1:Z5Iec2pjwb+LEOqzpB2MR12/eKFhDPhuqW91O+4bwUk= +google.golang.org/grpc v1.59.0/go.mod h1:aUPDwccQo6OTjy7Hct4AfBPD1GptF4fyUjIkQ9YtF98= +google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -579,72 +1820,104 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.27/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8= +gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= -helm.sh/helm/v3 v3.13.1 h1:DG+XLGzBJeZvMLlMbm6bPDLV1dGaVW9eZsDoUd1/LM0= -helm.sh/helm/v3 v3.13.1/go.mod h1:TdQRMiq46CSWcc68Hb0uVhvAWusaN90YwAV54cz6JzU= +helm.sh/helm/v3 v3.14.4 h1:6FSpEfqyDalHq3kUr4gOMThhgY55kXUEjdQoyODYnrM= +helm.sh/helm/v3 v3.14.4/go.mod h1:Tje7LL4gprZpuBNTbG34d1Xn5NmRT3OWfBRwpOSer9I= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.28.3 h1:Gj1HtbSdB4P08C8rs9AR94MfSGpRhJgsS+GF9V26xMM= -k8s.io/api v0.28.3/go.mod h1:MRCV/jr1dW87/qJnZ57U5Pak65LGmQVkKTzf3AtKFHc= -k8s.io/apiextensions-apiserver v0.28.3 h1:Od7DEnhXHnHPZG+W9I97/fSQkVpVPQx2diy+2EtmY08= -k8s.io/apiextensions-apiserver v0.28.3/go.mod h1:NE1XJZ4On0hS11aWWJUTNkmVB03j9LM7gJSisbRt8Lc= -k8s.io/apimachinery v0.28.3 h1:B1wYx8txOaCQG0HmYF6nbpU8dg6HvA06x5tEffvOe7A= -k8s.io/apimachinery v0.28.3/go.mod h1:uQTKmIqs+rAYaq+DFaoD2X7pcjLOqbQX2AOiO0nIpb8= -k8s.io/apiserver v0.28.3 h1:8Ov47O1cMyeDzTXz0rwcfIIGAP/dP7L8rWbEljRcg5w= -k8s.io/apiserver v0.28.3/go.mod h1:YIpM+9wngNAv8Ctt0rHG4vQuX/I5rvkEMtZtsxW2rNM= -k8s.io/cli-runtime v0.28.3 h1:lvuJYVkwCqHEvpS6KuTZsUVwPePFjBfSGvuaLl2SxzA= -k8s.io/cli-runtime v0.28.3/go.mod h1:jeX37ZPjIcENVuXDDTskG3+FnVuZms5D9omDXS/2Jjc= -k8s.io/client-go v0.28.3 h1:2OqNb72ZuTZPKCl+4gTKvqao0AMOl9f3o2ijbAj3LI4= -k8s.io/client-go v0.28.3/go.mod h1:LTykbBp9gsA7SwqirlCXBWtK0guzfhpoW4qSm7i9dxo= -k8s.io/component-base v0.28.3 h1:rDy68eHKxq/80RiMb2Ld/tbH8uAE75JdCqJyi6lXMzI= -k8s.io/component-base v0.28.3/go.mod h1:fDJ6vpVNSk6cRo5wmDa6eKIG7UlIQkaFmZN2fYgIUD8= -k8s.io/klog/v2 v2.100.1 h1:7WCHKK6K8fNhTqfBhISHQ97KrnJNFZMcQvKp7gP/tmg= -k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ= -k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM= -k8s.io/kubectl v0.28.3 h1:H1Peu1O3EbN9zHkJCcvhiJ4NUj6lb88sGPO5wrWIM6k= -k8s.io/kubectl v0.28.3/go.mod h1:RDAudrth/2wQ3Sg46fbKKl4/g+XImzvbsSRZdP2RiyE= -k8s.io/kubernetes v1.28.3 h1:XTci6gzk+JR51UZuZQCFJ4CsyUkfivSjLI4O1P9z6LY= -k8s.io/kubernetes v1.28.3/go.mod h1:NhAysZWvHtNcJFFHic87ofxQN7loylCQwg3ZvXVDbag= -k8s.io/metrics v0.28.3 h1:w2s3kVi7HulXqCVDFkF4hN/OsL1tXTTb4Biif995h/g= -k8s.io/metrics v0.28.3/go.mod h1:OZZ23AHFojPzU6r3xoHGRUcV3I9pauLua+07sAUbwLc= -k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk= -k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -oras.land/oras-go v1.2.4 h1:djpBY2/2Cs1PV87GSJlxv4voajVOMZxqqtq9AB8YNvY= -oras.land/oras-go v1.2.4/go.mod h1:DYcGfb3YF1nKjcezfX2SNlDAeQFKSXmf+qrFmrh4324= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.30.1 h1:kCm/6mADMdbAxmIh0LBjS54nQBE+U4KmbCfIkF5CpJY= +k8s.io/api v0.30.1/go.mod h1:ddbN2C0+0DIiPntan/bye3SW3PdwLa11/0yqwvuRrJM= +k8s.io/apiextensions-apiserver v0.30.1 h1:4fAJZ9985BmpJG6PkoxVRpXv9vmPUOVzl614xarePws= +k8s.io/apiextensions-apiserver v0.30.1/go.mod h1:R4GuSrlhgq43oRY9sF2IToFh7PVlF1JjfWdoG3pixk4= +k8s.io/apimachinery v0.30.1 h1:ZQStsEfo4n65yAdlGTfP/uSHMQSoYzU/oeEbkmF7P2U= +k8s.io/apimachinery v0.30.1/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/apiserver v0.30.1 h1:BEWEe8bzS12nMtDKXzCF5Q5ovp6LjjYkSp8qOPk8LZ8= +k8s.io/apiserver v0.30.1/go.mod h1:i87ZnQ+/PGAmSbD/iEKM68bm1D5reX8fO4Ito4B01mo= +k8s.io/cli-runtime v0.30.1 h1:kSBBpfrJGS6lllc24KeniI9JN7ckOOJKnmFYH1RpTOw= +k8s.io/cli-runtime v0.30.1/go.mod h1:zhHgbqI4J00pxb6gM3gJPVf2ysDjhQmQtnTxnMScab8= +k8s.io/client-go v0.30.1 h1:uC/Ir6A3R46wdkgCV3vbLyNOYyCJ8oZnjtJGKfytl/Q= +k8s.io/client-go v0.30.1/go.mod h1:wrAqLNs2trwiCH/wxxmT/x3hKVH9PuV0GGW0oDoHVqc= +k8s.io/component-base v0.30.1 h1:bvAtlPh1UrdaZL20D9+sWxsJljMi0QZ3Lmw+kmZAaxQ= +k8s.io/component-base v0.30.1/go.mod h1:e/X9kDiOebwlI41AvBHuWdqFriSRrX50CdwA9TFaHLI= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/kubectl v0.30.1 h1:sHFIRI3oP0FFZmBAVEE8ErjnTyXDPkBcvO88mH9RjuY= +k8s.io/kubectl v0.30.1/go.mod h1:7j+L0Cc38RYEcx+WH3y44jRBe1Q1jxdGPKkX0h4iDq0= +k8s.io/metrics v0.30.1 h1:PeA9cP0kxVtaC8Wkzp4sTkr7YSkd9R0UYP6cCHOOY1M= +k8s.io/metrics v0.30.1/go.mod h1:gVAhTTgfNKsn9D1kB7Nmb1T31relBuXzzGUE7klyOkM= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +modernc.org/libc v1.41.0 h1:g9YAc6BkKlgORsUWj+JwqoB1wU3o4DE3bM3yvA3k+Gk= +modernc.org/libc v1.41.0/go.mod h1:w0eszPsiXoOnoMJgrXjglgLuDy/bt5RR4y3QzUUeodY= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E= +modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E= +modernc.org/sqlite v1.29.6 h1:0lOXGrycJPptfHDuohfYgNqoe4hu+gYuN/pKgY5XjS4= +modernc.org/sqlite v1.29.6/go.mod h1:S02dvcmm7TnTRvGhv8IGYyLnIt7AS2KPaB1F/71p75U= +oras.land/oras-go v1.2.5 h1:XpYuAwAb0DfQsunIyMfeET92emK8km3W4yEzZvUbsTo= +oras.land/oras-go v1.2.5/go.mod h1:PuAwRShRZCsZb7g8Ar3jKKQR/2A/qN+pkYxIOd/FAoo= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 h1:XX3Ajgzov2RKUdc5jW3t5jwY7Bo7dcRm+tFxT+NfgY0= sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3/go.mod h1:9n16EZKMhXBNSiUC5kSdFQJkdH3zbxS/JoO619G1VAY= sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 h1:W6cLQc5pnqM7vh3b7HvGNfXrJ/xL6BDMS0v1V/HHg5U= sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3/go.mod h1:JWP1Fj0VWGHyw3YUPjXSQnRnrwezrZSrApfX5S0nIag= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= -sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= -sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/internal/client/client.go b/internal/client/client.go index a7fcf55889..e43a561aa6 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package client import ( "context" "errors" "fmt" + "os" "path/filepath" "strings" "sync" @@ -11,7 +15,6 @@ import ( "github.com/rs/zerolog/log" authorizationv1 "k8s.io/api/authorization/v1" - v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/cache" "k8s.io/apimachinery/pkg/version" @@ -19,7 +22,6 @@ import ( "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" restclient "k8s.io/client-go/rest" - cmdutil "k8s.io/kubectl/pkg/cmd/util" metricsapi "k8s.io/metrics/pkg/apis/metrics" "k8s.io/metrics/pkg/client/clientset/versioned" ) @@ -29,12 +31,13 @@ const ( cacheExpiry = 5 * time.Minute cacheMXAPIKey = "metricsAPI" serverVersion = "serverVersion" + cacheNSKey = "validNamespaces" ) var supportedMetricsAPIVersions = []string{"v1beta1"} -// Namespaces tracks a collection of namespace names. -type Namespaces map[string]struct{} +// NamespaceNames tracks a collection of namespace names. +type NamespaceNames map[string]struct{} // APIClient represents a Kubernetes api client. type APIClient struct { @@ -44,7 +47,7 @@ type APIClient struct { mxsClient *versioned.Clientset cachedClient *disk.CachedDiscoveryClient config *Config - mx sync.Mutex + mx sync.RWMutex cache *cache.LRUExpireCache connOK bool } @@ -69,7 +72,7 @@ func InitConnection(config *Config) (*APIClient, error) { if err != nil { log.Error().Err(err).Msgf("Fail to locate metrics-server") } - if errors.Is(err, noMetricServerErr) || errors.Is(err, metricsUnsupportedErr) { + if err == nil || errors.Is(err, noMetricServerErr) || errors.Is(err, metricsUnsupportedErr) { return &a, nil } a.connOK = false @@ -82,9 +85,9 @@ func (a *APIClient) ConnectionOK() bool { return a.connOK } -func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview { +func makeSAR(ns, gvr, name string) *authorizationv1.SelfSubjectAccessReview { if ns == ClusterScope { - ns = AllNamespaces + ns = BlankNamespace } spec := NewGVR(gvr) res := spec.GVR() @@ -96,18 +99,19 @@ func makeSAR(ns, gvr string) *authorizationv1.SelfSubjectAccessReview { Version: res.Version, Resource: res.Resource, Subresource: spec.SubResource(), + Name: name, }, }, } } -func makeCacheKey(ns, gvr string, vv []string) string { - return ns + ":" + gvr + "::" + strings.Join(vv, ",") +func makeCacheKey(ns, gvr, n string, vv []string) string { + return ns + ":" + gvr + ":" + n + "::" + strings.Join(vv, ",") } -// ActiveCluster returns the current cluster name. -func (a *APIClient) ActiveCluster() string { - c, err := a.config.CurrentClusterName() +// ActiveContext returns the current context name. +func (a *APIClient) ActiveContext() string { + c, err := a.config.CurrentContextName() if err != nil { log.Error().Msgf("Unable to located active cluster") return "" @@ -117,9 +121,10 @@ func (a *APIClient) ActiveCluster() string { // IsActiveNamespace returns true if namespaces matches. func (a *APIClient) IsActiveNamespace(ns string) bool { - if a.ActiveNamespace() == AllNamespaces { + if a.ActiveNamespace() == BlankNamespace { return true } + return a.ActiveNamespace() == ns } @@ -129,7 +134,7 @@ func (a *APIClient) ActiveNamespace() string { return ns } - return AllNamespaces + return BlankNamespace } func (a *APIClient) clearCache() { @@ -139,17 +144,14 @@ func (a *APIClient) clearCache() { } // CanI checks if user has access to a certain resource. -func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error) { - a.mx.Lock() - defer a.mx.Unlock() - - if !a.connOK { +func (a *APIClient) CanI(ns, gvr, name string, verbs []string) (auth bool, err error) { + if !a.getConnOK() { return false, errors.New("ACCESS -- No API server connection") } if IsClusterWide(ns) { - ns = AllNamespaces + ns = BlankNamespace } - key := makeCacheKey(ns, gvr, verbs) + key := makeCacheKey(ns, gvr, name, verbs) if v, ok := a.cache.Get(key); ok { if auth, ok = v.(bool); ok { return auth, nil @@ -160,14 +162,19 @@ func (a *APIClient) CanI(ns, gvr string, verbs []string) (auth bool, err error) if err != nil { return false, err } - client, sar := dial.AuthorizationV1().SelfSubjectAccessReviews(), makeSAR(ns, gvr) + client, sar := dial.AuthorizationV1().SelfSubjectAccessReviews(), makeSAR(ns, gvr, name) ctx, cancel := context.WithTimeout(context.Background(), a.config.CallTimeout()) defer cancel() for _, v := range verbs { sar.Spec.ResourceAttributes.Verb = v resp, err := client.Create(ctx, sar, metav1.CreateOptions{}) - log.Trace().Msgf("[CAN] %s(%s) %v <<%v>>", gvr, verbs, resp, err) + log.Trace().Msgf("[CAN] %s(%q/%q) <%v>", gvr, ns, name, verbs) + if resp != nil { + log.Trace().Msgf(" Spec: %#v", resp.Spec) + log.Trace().Msgf(" Auth: %t [%q]", resp.Status.Allowed, resp.Status.Reason) + } + log.Trace().Msgf(" <<%v>>", err) if err != nil { log.Warn().Err(err).Msgf(" Dial Failed!") a.cache.Add(key, false, cacheExpiry) @@ -210,17 +217,45 @@ func (a *APIClient) ServerVersion() (*version.Info, error) { return info, nil } -// ValidNamespaces returns all available namespaces. -func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) { +func (a *APIClient) IsValidNamespace(ns string) bool { + ok, err := a.isValidNamespace(ns) + if err != nil { + log.Warn().Err(err).Msgf("namespace validation failed for: %q", ns) + } + + return ok +} + +func (a *APIClient) isValidNamespace(n string) (bool, error) { + if IsClusterWide(n) || n == NotNamespaced { + return true, nil + } + nn, err := a.ValidNamespaceNames() + if err != nil { + return false, err + } + _, ok := nn[n] + + return ok, nil +} + +// ValidNamespaceNames returns all available namespaces. +func (a *APIClient) ValidNamespaceNames() (NamespaceNames, error) { if a == nil { return nil, fmt.Errorf("validNamespaces: no available client found") } - if nn, ok := a.cache.Get("validNamespaces"); ok { - if nss, ok := nn.([]v1.Namespace); ok { + if nn, ok := a.cache.Get(cacheNSKey); ok { + if nss, ok := nn.(NamespaceNames); ok { return nss, nil } } + + ok, err := a.CanI(ClusterScope, "v1/namespaces", "", ListAccess) + if !ok || err != nil { + return nil, fmt.Errorf("user not authorized to list all namespaces") + } + dial, err := a.Dial() if err != nil { return nil, err @@ -231,21 +266,22 @@ func (a *APIClient) ValidNamespaces() ([]v1.Namespace, error) { if err != nil { return nil, err } - a.cache.Add("validNamespaces", nn.Items, cacheExpiry) + nns := make(NamespaceNames, len(nn.Items)) + for _, n := range nn.Items { + nns[n.Name] = struct{}{} + } + a.cache.Add(cacheNSKey, nns, cacheExpiry) - return nn.Items, nil + return nns, nil } // CheckConnectivity return true if api server is cool or false otherwise. func (a *APIClient) CheckConnectivity() bool { - a.mx.Lock() - defer a.mx.Unlock() - defer func() { if err := recover(); err != nil { - a.connOK = false + a.setConnOK(false) } - if !a.connOK { + if !a.getConnOK() { a.clearCache() } }() @@ -261,21 +297,21 @@ func (a *APIClient) CheckConnectivity() bool { client, err := kubernetes.NewForConfig(cfg) if err != nil { log.Error().Err(err).Msgf("Unable to connect to api server") - a.connOK = false - return a.connOK + a.setConnOK(false) + return a.getConnOK() } // Check connection if _, err := client.ServerVersion(); err == nil { - if !a.connOK { + if !a.getConnOK() { a.reset() } } else { log.Error().Err(err).Msgf("can't connect to cluster") - a.connOK = false + a.setConnOK(false) } - return a.connOK + return a.getConnOK() } // Config return a kubernetes configuration. @@ -285,20 +321,100 @@ func (a *APIClient) Config() *Config { // HasMetrics checks if the cluster supports metrics. func (a *APIClient) HasMetrics() bool { - err := a.supportsMetricsResources() - if err != nil { - log.Debug().Msgf("Metrics server detect failed: %s", err) - } - return err == nil + return a.supportsMetricsResources() == nil +} + +func (a *APIClient) getMxsClient() *versioned.Clientset { + a.mx.RLock() + defer a.mx.RUnlock() + + return a.mxsClient +} + +func (a *APIClient) setMxsClient(c *versioned.Clientset) { + a.mx.Lock() + defer a.mx.Unlock() + + a.mxsClient = c +} + +func (a *APIClient) getCachedClient() *disk.CachedDiscoveryClient { + a.mx.RLock() + defer a.mx.RUnlock() + + return a.cachedClient +} + +func (a *APIClient) setCachedClient(c *disk.CachedDiscoveryClient) { + a.mx.Lock() + defer a.mx.Unlock() + + a.cachedClient = c +} + +func (a *APIClient) getDClient() dynamic.Interface { + a.mx.RLock() + defer a.mx.RUnlock() + + return a.dClient +} + +func (a *APIClient) setDClient(c dynamic.Interface) { + a.mx.Lock() + defer a.mx.Unlock() + + a.dClient = c +} + +func (a *APIClient) getConnOK() bool { + a.mx.RLock() + defer a.mx.RUnlock() + + return a.connOK +} + +func (a *APIClient) setConnOK(b bool) { + a.mx.Lock() + defer a.mx.Unlock() + + a.connOK = b +} + +func (a *APIClient) setLogClient(k kubernetes.Interface) { + a.mx.Lock() + defer a.mx.Unlock() + + a.logClient = k +} + +func (a *APIClient) getLogClient() kubernetes.Interface { + a.mx.RLock() + defer a.mx.RUnlock() + + return a.logClient +} + +func (a *APIClient) setClient(k kubernetes.Interface) { + a.mx.Lock() + defer a.mx.Unlock() + + a.client = k +} + +func (a *APIClient) getClient() kubernetes.Interface { + a.mx.RLock() + defer a.mx.RUnlock() + + return a.client } // DialLogs returns a handle to api server for logs. func (a *APIClient) DialLogs() (kubernetes.Interface, error) { - if !a.connOK { - return nil, errors.New("no connection to dial") + if !a.getConnOK() { + return nil, errors.New("dialLogs - no connection to dial") } - if a.logClient != nil { - return a.logClient, nil + if clt := a.getLogClient(); clt != nil { + return clt, nil } cfg, err := a.RestConfig() @@ -306,31 +422,35 @@ func (a *APIClient) DialLogs() (kubernetes.Interface, error) { return nil, err } cfg.Timeout = 0 - if a.logClient, err = kubernetes.NewForConfig(cfg); err != nil { + c, err := kubernetes.NewForConfig(cfg) + if err != nil { return nil, err } + a.setLogClient(c) - return a.logClient, nil + return a.getLogClient(), nil } // Dial returns a handle to api server or die. func (a *APIClient) Dial() (kubernetes.Interface, error) { - if !a.connOK { + if !a.getConnOK() { return nil, errors.New("no connection to dial") } - if a.client != nil { - return a.client, nil + if c := a.getClient(); c != nil { + return c, nil } cfg, err := a.RestConfig() if err != nil { return nil, err } - if a.client, err = kubernetes.NewForConfig(cfg); err != nil { + if c, err := kubernetes.NewForConfig(cfg); err != nil { return nil, err + } else { + a.setClient(c) } - return a.client, nil + return a.getClient(), nil } // RestConfig returns a rest api client. @@ -340,15 +460,12 @@ func (a *APIClient) RestConfig() (*restclient.Config, error) { // CachedDiscovery returns a cached discovery client. func (a *APIClient) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { - a.mx.Lock() - defer a.mx.Unlock() - - if !a.connOK { + if !a.getConnOK() { return nil, errors.New("no connection to cached dial") } - if a.cachedClient != nil { - return a.cachedClient, nil + if c := a.getCachedClient(); c != nil { + return c, nil } cfg, err := a.RestConfig() @@ -356,40 +473,46 @@ func (a *APIClient) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { return nil, err } - httpCacheDir := filepath.Join(mustHomeDir(), ".kube", "http-cache") - discCacheDir := filepath.Join(mustHomeDir(), ".kube", "cache", "discovery", toHostDir(cfg.Host)) + baseCacheDir := os.Getenv("KUBECACHEDIR") + if baseCacheDir == "" { + baseCacheDir = filepath.Join(mustHomeDir(), ".kube", "cache") + } + + httpCacheDir := filepath.Join(baseCacheDir, "http") + discCacheDir := filepath.Join(baseCacheDir, "discovery", toHostDir(cfg.Host)) - a.cachedClient, err = disk.NewCachedDiscoveryClientForConfig(cfg, discCacheDir, httpCacheDir, cacheExpiry) + c, err := disk.NewCachedDiscoveryClientForConfig(cfg, discCacheDir, httpCacheDir, cacheExpiry) if err != nil { return nil, err } - return a.cachedClient, nil + a.setCachedClient(c) + + return a.getCachedClient(), nil } // DynDial returns a handle to a dynamic interface. func (a *APIClient) DynDial() (dynamic.Interface, error) { - if a.dClient != nil { - return a.dClient, nil + if c := a.getDClient(); c != nil { + return c, nil } cfg, err := a.RestConfig() if err != nil { return nil, err } - if a.dClient, err = dynamic.NewForConfig(cfg); err != nil { - log.Panic().Err(err) + c, err := dynamic.NewForConfig(cfg) + if err != nil { + return nil, err } + a.setDClient(c) - return a.dClient, nil + return a.getDClient(), nil } // MXDial returns a handle to the metrics server. func (a *APIClient) MXDial() (*versioned.Clientset, error) { - a.mx.Lock() - defer a.mx.Unlock() - - if a.mxsClient != nil { - return a.mxsClient, nil + if c := a.getMxsClient(); c != nil { + return c, nil } cfg, err := a.RestConfig() @@ -397,11 +520,23 @@ func (a *APIClient) MXDial() (*versioned.Clientset, error) { return nil, err } - if a.mxsClient, err = versioned.NewForConfig(cfg); err != nil { - log.Error().Err(err) + c, err := versioned.NewForConfig(cfg) + if err != nil { + return nil, err } + a.setMxsClient(c) - return a.mxsClient, err + return a.getMxsClient(), err +} + +func (a *APIClient) invalidateCache() error { + dial, err := a.CachedDiscovery() + if err != nil { + return err + } + dial.Invalidate() + + return nil } // SwitchContext handles kubeconfig context switches. @@ -410,12 +545,11 @@ func (a *APIClient) SwitchContext(name string) error { if err := a.config.SwitchContext(name); err != nil { return err } - a.mx.Lock() - { - a.reset() - ResetMetrics() + if err := a.invalidateCache(); err != nil { + return err } - a.mx.Unlock() + a.reset() + ResetMetrics() if !a.CheckConnectivity() { return fmt.Errorf("unable to connect to context %q", name) @@ -427,9 +561,14 @@ func (a *APIClient) SwitchContext(name string) error { func (a *APIClient) reset() { a.config.reset() a.cache = cache.NewLRUExpireCache(cacheSize) - a.client, a.dClient, a.nsClient, a.mxsClient = nil, nil, nil, nil - a.cachedClient, a.logClient = nil, nil - a.connOK = true + a.nsClient = nil + + a.setDClient(nil) + a.setMxsClient(nil) + a.setCachedClient(nil) + a.setClient(nil) + a.setLogClient(nil) + a.setConnOK(true) } func (a *APIClient) checkCacheBool(key string) (state bool, ok bool) { @@ -454,14 +593,12 @@ func (a *APIClient) supportsMetricsResources() error { a.cache.Add(cacheMXAPIKey, supported, cacheExpiry) }() - cfg := cmdutil.NewMatchVersionFlags(a.config.flags) - f := cmdutil.NewFactory(cfg) - dial, err := f.ToDiscoveryClient() + dial, err := a.Dial() if err != nil { log.Warn().Err(err).Msgf("Unable to dial discovery API") return err } - apiGroups, err := dial.ServerGroups() + apiGroups, err := dial.Discovery().ServerGroups() if err != nil { return err } diff --git a/internal/client/client_test.go b/internal/client/client_test.go index a489f46784..e60634880e 100644 --- a/internal/client/client_test.go +++ b/internal/client/client_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package client import ( @@ -5,8 +8,145 @@ import ( "time" "github.com/stretchr/testify/assert" + authorizationv1 "k8s.io/api/authorization/v1" ) +func TestMakeSAR(t *testing.T) { + uu := map[string]struct { + ns string + gvr GVR + sar *authorizationv1.SelfSubjectAccessReview + }{ + "all-pods": { + ns: NamespaceAll, + gvr: NewGVR("v1/pods"), + sar: &authorizationv1.SelfSubjectAccessReview{ + Spec: authorizationv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authorizationv1.ResourceAttributes{ + Namespace: NamespaceAll, + Version: "v1", + Resource: "pods", + }, + }, + }, + }, + "ns-pods": { + ns: "fred", + gvr: NewGVR("v1/pods"), + sar: &authorizationv1.SelfSubjectAccessReview{ + Spec: authorizationv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authorizationv1.ResourceAttributes{ + Namespace: "fred", + Version: "v1", + Resource: "pods", + }, + }, + }, + }, + "clusterscope-ns": { + ns: ClusterScope, + gvr: NewGVR("v1/namespaces"), + sar: &authorizationv1.SelfSubjectAccessReview{ + Spec: authorizationv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authorizationv1.ResourceAttributes{ + Version: "v1", + Resource: "namespaces", + }, + }, + }, + }, + "subres-pods": { + ns: "fred", + gvr: NewGVR("v1/pods:logs"), + sar: &authorizationv1.SelfSubjectAccessReview{ + Spec: authorizationv1.SelfSubjectAccessReviewSpec{ + ResourceAttributes: &authorizationv1.ResourceAttributes{ + Namespace: "fred", + Version: "v1", + Resource: "pods", + Subresource: "logs", + }, + }, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.sar, makeSAR(u.ns, u.gvr.String(), "")) + }) + } +} + +func TestIsValidNamespace(t *testing.T) { + c := NewTestAPIClient() + + uu := map[string]struct { + ns string + cache NamespaceNames + ok bool + }{ + "all-ns": { + ns: NamespaceAll, + cache: NamespaceNames{ + DefaultNamespace: {}, + }, + ok: true, + }, + "blank-ns": { + ns: BlankNamespace, + cache: NamespaceNames{ + DefaultNamespace: {}, + }, + ok: true, + }, + "cluster-ns": { + ns: ClusterScope, + cache: NamespaceNames{ + DefaultNamespace: {}, + }, + ok: true, + }, + "no-ns": { + ns: NotNamespaced, + cache: NamespaceNames{ + DefaultNamespace: {}, + }, + ok: true, + }, + "default-ns": { + ns: DefaultNamespace, + cache: NamespaceNames{ + DefaultNamespace: {}, + }, + ok: true, + }, + "valid-ns": { + ns: "fred", + cache: NamespaceNames{ + "fred": {}, + }, + ok: true, + }, + "invalid-ns": { + ns: "fred", + cache: NamespaceNames{ + DefaultNamespace: {}, + }, + }, + } + + expiry := 1 * time.Millisecond + for k := range uu { + u := uu[k] + c.cache.Add("validNamespaces", u.cache, expiry) + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.ok, c.IsValidNamespace(u.ns)) + }) + } +} + func TestCheckCacheBool(t *testing.T) { c := NewTestAPIClient() diff --git a/internal/client/config.go b/internal/client/config.go index 6086ac3159..65c3d6d777 100644 --- a/internal/client/config.go +++ b/internal/client/config.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package client import ( @@ -7,15 +10,14 @@ import ( "sync" "time" - v1 "k8s.io/api/core/v1" "k8s.io/cli-runtime/pkg/genericclioptions" restclient "k8s.io/client-go/rest" clientcmd "k8s.io/client-go/tools/clientcmd" - clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/client-go/tools/clientcmd/api" ) const ( - defaultCallTimeoutDuration time.Duration = 10 * time.Second + defaultCallTimeoutDuration time.Duration = 15 * time.Second // UsePersistentConfig caches client config to avoid reloads. UsePersistentConfig = true @@ -24,14 +26,13 @@ const ( // Config tracks a kubernetes configuration. type Config struct { flags *genericclioptions.ConfigFlags - mutex *sync.RWMutex + mx sync.RWMutex } // NewConfig returns a new k8s config or an error if the flags are invalid. func NewConfig(f *genericclioptions.ConfigFlags) *Config { return &Config{ flags: f, - mutex: &sync.RWMutex{}, } } @@ -57,7 +58,7 @@ func (c *Config) Flags() *genericclioptions.ConfigFlags { return c.flags } -func (c *Config) RawConfig() (clientcmdapi.Config, error) { +func (c *Config) RawConfig() (api.Config, error) { return c.clientConfig().RawConfig() } @@ -69,11 +70,14 @@ func (c *Config) reset() {} // SwitchContext changes the kubeconfig context to a new cluster. func (c *Config) SwitchContext(name string) error { - if _, err := c.GetContext(name); err != nil { + ct, err := c.GetContext(name) + if err != nil { return fmt.Errorf("context %q does not exist", name) } + // !!BOZO!! Do you need to reset the flags? flags := genericclioptions.NewConfigFlags(UsePersistentConfig) - flags.Context = &name + flags.Context, flags.ClusterName = &name, &ct.Cluster + flags.Namespace = c.flags.Namespace flags.Timeout = c.flags.Timeout flags.KubeConfig = c.flags.KubeConfig c.flags = flags @@ -81,6 +85,49 @@ func (c *Config) SwitchContext(name string) error { return nil } +func (c *Config) Clone(ns string) (*genericclioptions.ConfigFlags, error) { + flags := genericclioptions.NewConfigFlags(false) + ct, err := c.CurrentContextName() + if err != nil { + return nil, err + } + cl, err := c.CurrentClusterName() + if err != nil { + return nil, err + } + flags.Context, flags.ClusterName = &ct, &cl + flags.Namespace = &ns + flags.Timeout = c.Flags().Timeout + flags.KubeConfig = c.Flags().KubeConfig + + return flags, nil +} + +// CurrentClusterName returns the currently active cluster name. +func (c *Config) CurrentClusterName() (string, error) { + if isSet(c.flags.ClusterName) { + return *c.flags.ClusterName, nil + } + cfg, err := c.RawConfig() + if err != nil { + return "", err + } + + ct, ok := cfg.Contexts[cfg.CurrentContext] + if !ok { + return "", fmt.Errorf("invalid current context specified: %q", cfg.CurrentContext) + } + if isSet(c.flags.Context) { + ct, ok = cfg.Contexts[*c.flags.Context] + if !ok { + return "", fmt.Errorf("current-cluster - invalid context specified: %q", *c.flags.Context) + } + } + + return ct.Cluster, nil + +} + // CurrentContextName returns the currently active config context. func (c *Config) CurrentContextName() (string, error) { if isSet(c.flags.Context) { @@ -88,7 +135,7 @@ func (c *Config) CurrentContextName() (string, error) { } cfg, err := c.RawConfig() if err != nil { - return "", err + return "", fmt.Errorf("fail to load rawConfig: %w", err) } return cfg.CurrentContext, nil @@ -107,8 +154,17 @@ func (c *Config) CurrentContextNamespace() (string, error) { return context.Namespace, nil } +// CurrentContext returns the current context configuration. +func (c *Config) CurrentContext() (*api.Context, error) { + n, err := c.CurrentContextName() + if err != nil { + return nil, err + } + return c.GetContext(n) +} + // GetContext fetch a given context or error if it does not exists. -func (c *Config) GetContext(n string) (*clientcmdapi.Context, error) { +func (c *Config) GetContext(n string) (*api.Context, error) { cfg, err := c.RawConfig() if err != nil { return nil, err @@ -117,11 +173,11 @@ func (c *Config) GetContext(n string) (*clientcmdapi.Context, error) { return c, nil } - return nil, fmt.Errorf("invalid context `%s specified", n) + return nil, fmt.Errorf("getcontext - invalid context specified: %q", n) } // Contexts fetch all available contexts. -func (c *Config) Contexts() (map[string]*clientcmdapi.Context, error) { +func (c *Config) Contexts() (map[string]*api.Context, error) { cfg, err := c.RawConfig() if err != nil { return nil, err @@ -177,63 +233,14 @@ func (c *Config) RenameContext(old string, new string) error { } // ContextNames fetch all available contexts. -func (c *Config) ContextNames() ([]string, error) { +func (c *Config) ContextNames() (map[string]struct{}, error) { cfg, err := c.RawConfig() if err != nil { return nil, err } - - cc := make([]string, 0, len(cfg.Contexts)) + cc := make(map[string]struct{}, len(cfg.Contexts)) for n := range cfg.Contexts { - cc = append(cc, n) - } - return cc, nil -} - -// ClusterNameFromContext returns the cluster associated with the given context. -func (c *Config) ClusterNameFromContext(context string) (string, error) { - cfg, err := c.RawConfig() - if err != nil { - return "", err - } - - if ctx, ok := cfg.Contexts[context]; ok { - return ctx.Cluster, nil - } - return "", fmt.Errorf("unable to locate cluster from context %s", context) -} - -// CurrentClusterName returns the active cluster name. -func (c *Config) CurrentClusterName() (string, error) { - if isSet(c.flags.ClusterName) { - return *c.flags.ClusterName, nil - } - cfg, err := c.RawConfig() - if err != nil { - return "", err - } - context, err := c.CurrentContextName() - if err != nil { - context = cfg.CurrentContext - } - - if ctx, ok := cfg.Contexts[context]; ok { - return ctx.Cluster, nil - } - - return "", errors.New("unable to locate current cluster") -} - -// ClusterNames fetch all kubeconfig defined clusters. -func (c *Config) ClusterNames() (map[string]struct{}, error) { - cfg, err := c.RawConfig() - if err != nil { - return nil, err - } - - cc := make(map[string]struct{}, len(cfg.Clusters)) - for name := range cfg.Clusters { - cc[name] = struct{}{} + cc[n] = struct{}{} } return cc, nil @@ -294,22 +301,23 @@ func (c *Config) CurrentUserName() (string, error) { // CurrentNamespaceName retrieves the active namespace. func (c *Config) CurrentNamespaceName() (string, error) { - ns, _, err := c.clientConfig().Namespace() - - if ns == "default" { - ns, err = c.CurrentContextNamespace() - if ns == "" && err == nil { - return "", errors.New("No namespace specified in context") - } + ns, overridden, err := c.clientConfig().Namespace() + if err != nil { + return BlankNamespace, err + } + // Checks if ns is passed is in args. + if overridden { + return ns, nil } - return ns, err + // Return ns set in context if any?? + return c.CurrentContextNamespace() } // ConfigAccess return the current kubeconfig api server access configuration. func (c *Config) ConfigAccess() (clientcmd.ConfigAccess, error) { - c.mutex.RLock() - defer c.mutex.RUnlock() + c.mx.RLock() + defer c.mx.RUnlock() return c.clientConfig().ConfigAccess(), nil } @@ -317,16 +325,6 @@ func (c *Config) ConfigAccess() (clientcmd.ConfigAccess, error) { // ---------------------------------------------------------------------------- // Helpers... -// NamespaceNames fetch all available namespaces on current cluster. -func NamespaceNames(nns []v1.Namespace) []string { - nn := make([]string, 0, len(nns)) - for _, ns := range nns { - nn = append(nn, ns.Name) - } - - return nn -} - func isSet(s *string) bool { return s != nil && len(*s) != 0 } diff --git a/internal/client/config_test.go b/internal/client/config_test.go index cf2e854945..c92593aff8 100644 --- a/internal/client/config_test.go +++ b/internal/client/config_test.go @@ -1,14 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package client_test import ( "errors" + "os" "testing" + "time" "github.com/derailed/k9s/internal/client" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" ) @@ -16,6 +19,31 @@ func init() { zerolog.SetGlobalLevel(zerolog.FatalLevel) } +func TestCallTimeout(t *testing.T) { + uu := map[string]struct { + t string + e time.Duration + }{ + "custom": { + t: "1m", + e: 1 * time.Minute, + }, + "default": { + e: 15 * time.Second, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + flags := genericclioptions.NewConfigFlags(false) + flags.Timeout = &u.t + cfg := client.NewConfig(flags) + assert.Equal(t, u.e, cfg.CallTimeout()) + }) + } +} + func TestConfigCurrentContext(t *testing.T) { var kubeConfig = "./testdata/config" @@ -55,11 +83,16 @@ func TestConfigCurrentCluster(t *testing.T) { cluster string }{ "default": { - flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig}, - cluster: "fred", + flags: &genericclioptions.ConfigFlags{ + KubeConfig: &kubeConfig, + }, + cluster: "zorg", }, "custom": { - flags: &genericclioptions.ConfigFlags{KubeConfig: &kubeConfig, ClusterName: &name}, + flags: &genericclioptions.ConfigFlags{ + KubeConfig: &kubeConfig, + Context: &name, + }, cluster: "blee", }, } @@ -68,9 +101,9 @@ func TestConfigCurrentCluster(t *testing.T) { u := uu[k] t.Run(k, func(t *testing.T) { cfg := client.NewConfig(u.flags) - ctx, err := cfg.CurrentClusterName() + ct, err := cfg.CurrentClusterName() assert.Nil(t, err) - assert.Equal(t, u.cluster, ctx) + assert.Equal(t, u.cluster, ct) }) } } @@ -147,7 +180,7 @@ func TestConfigGetContext(t *testing.T) { }, "custom": { cluster: "bozo", - err: errors.New("invalid context `bozo specified"), + err: errors.New(`getcontext - invalid context specified: "bozo"`), }, } @@ -170,8 +203,8 @@ func TestConfigGetContext(t *testing.T) { func TestConfigSwitchContext(t *testing.T) { cluster, kubeConfig := "duh", "./testdata/config" flags := genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfig, - ClusterName: &cluster, + KubeConfig: &kubeConfig, + Context: &cluster, } cfg := client.NewConfig(&flags) @@ -182,24 +215,11 @@ func TestConfigSwitchContext(t *testing.T) { assert.Equal(t, "blee", ctx) } -func TestConfigClusterNameFromContext(t *testing.T) { - cluster, kubeConfig := "duh", "./testdata/config" - flags := genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfig, - ClusterName: &cluster, - } - - cfg := client.NewConfig(&flags) - cl, err := cfg.ClusterNameFromContext("blee") - assert.Nil(t, err) - assert.Equal(t, "blee", cl) -} - func TestConfigAccess(t *testing.T) { - cluster, kubeConfig := "duh", "./testdata/config" + context, kubeConfig := "duh", "./testdata/config" flags := genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfig, - ClusterName: &cluster, + KubeConfig: &kubeConfig, + Context: &context, } cfg := client.NewConfig(&flags) @@ -208,24 +228,11 @@ func TestConfigAccess(t *testing.T) { assert.True(t, len(acc.GetDefaultFilename()) > 0) } -func TestConfigContexts(t *testing.T) { - cluster, kubeConfig := "duh", "./testdata/config" - flags := genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfig, - ClusterName: &cluster, - } - - cfg := client.NewConfig(&flags) - cc, err := cfg.Contexts() - assert.Nil(t, err) - assert.Equal(t, 3, len(cc)) -} - func TestConfigContextNames(t *testing.T) { cluster, kubeConfig := "duh", "./testdata/config" flags := genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfig, - ClusterName: &cluster, + KubeConfig: &kubeConfig, + Context: &cluster, } cfg := client.NewConfig(&flags) @@ -234,33 +241,37 @@ func TestConfigContextNames(t *testing.T) { assert.Equal(t, 3, len(cc)) } -func TestConfigClusterNames(t *testing.T) { - cluster, kubeConfig := "duh", "./testdata/config" +func TestConfigContexts(t *testing.T) { + context, kubeConfig := "duh", "./testdata/config" flags := genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfig, - ClusterName: &cluster, + KubeConfig: &kubeConfig, + Context: &context, } cfg := client.NewConfig(&flags) - cc, err := cfg.ClusterNames() + cc, err := cfg.Contexts() assert.Nil(t, err) assert.Equal(t, 3, len(cc)) } func TestConfigDelContext(t *testing.T) { - cluster, kubeConfig := "duh", "./testdata/config.1" + assert.NoError(t, cp("./testdata/config.2", "./testdata/config.1")) + + context, kubeConfig := "duh", "./testdata/config.1" flags := genericclioptions.ConfigFlags{ - KubeConfig: &kubeConfig, - ClusterName: &cluster, + KubeConfig: &kubeConfig, + Context: &context, } cfg := client.NewConfig(&flags) err := cfg.DelContext("fred") - assert.Nil(t, err) + assert.NoError(t, err) + cc, err := cfg.ContextNames() - assert.Nil(t, err) + assert.NoError(t, err) assert.Equal(t, 1, len(cc)) - assert.Equal(t, "blee", cc[0]) + _, ok := cc["blee"] + assert.True(t, ok) } func TestConfigRestConfig(t *testing.T) { @@ -272,7 +283,7 @@ func TestConfigRestConfig(t *testing.T) { cfg := client.NewConfig(&flags) rc, err := cfg.RESTConfig() assert.Nil(t, err) - assert.Equal(t, "https://localhost:3000", rc.Host) + assert.Equal(t, "https://localhost:3002", rc.Host) } func TestConfigBadConfig(t *testing.T) { @@ -286,13 +297,13 @@ func TestConfigBadConfig(t *testing.T) { assert.NotNil(t, err) } -func TestNamespaceNames(t *testing.T) { - nn := []v1.Namespace{ - {ObjectMeta: metav1.ObjectMeta{Name: "ns1"}}, - {ObjectMeta: metav1.ObjectMeta{Name: "ns2"}}, +// Helpers... + +func cp(src string, dst string) error { + data, err := os.ReadFile(src) + if err != nil { + return err } - nns := client.NamespaceNames(nn) - assert.Equal(t, 2, len(nns)) - assert.Equal(t, []string{"ns1", "ns2"}, nns) + return os.WriteFile(dst, data, 0600) } diff --git a/internal/client/errors.go b/internal/client/errors.go index 99dabe3d09..f13260ddf0 100644 --- a/internal/client/errors.go +++ b/internal/client/errors.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package client import metricsapi "k8s.io/metrics/pkg/apis/metrics" diff --git a/internal/client/gvr.go b/internal/client/gvr.go index 6822e93d3b..853efb80ae 100644 --- a/internal/client/gvr.go +++ b/internal/client/gvr.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package client import ( @@ -11,6 +14,8 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" ) +var NoGVR = GVR{} + // GVR represents a kubernetes resource schema as a string. // Format is group/version/resources:subresource. type GVR struct { @@ -130,6 +135,11 @@ func (g GVR) G() string { return g.g } +// IsDecodable checks if the k8s resource has a decodable view +func (g GVR) IsDecodable() bool { + return g.GVK().Kind == "secrets" +} + // GVRs represents a collection of gvr. type GVRs []GVR diff --git a/internal/client/gvr_test.go b/internal/client/gvr_test.go index 224ee2824e..744ef26773 100644 --- a/internal/client/gvr_test.go +++ b/internal/client/gvr_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package client_test import ( @@ -46,7 +49,7 @@ func TestGVRCan(t *testing.T) { } } -func TestAsGVR(t *testing.T) { +func TestGVR(t *testing.T) { uu := map[string]struct { gvr string e schema.GroupVersionResource diff --git a/internal/client/helper_test.go b/internal/client/helper_test.go index 4a4c509161..6103e5eaba 100644 --- a/internal/client/helper_test.go +++ b/internal/client/helper_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package client_test import ( @@ -5,8 +8,193 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +func TestMetaFQN(t *testing.T) { + uu := map[string]struct { + meta metav1.ObjectMeta + e string + }{ + "empty": { + e: "-/", + }, + "full": { + meta: metav1.ObjectMeta{Name: "blee", Namespace: "ns1"}, + e: "ns1/blee", + }, + "no-ns": { + meta: metav1.ObjectMeta{Name: "blee"}, + e: "-/blee", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, client.MetaFQN(u.meta)) + }) + } +} + +func TestCoFQN(t *testing.T) { + uu := map[string]struct { + meta metav1.ObjectMeta + co string + e string + }{ + "empty": { + e: "-/:", + }, + "full": { + meta: metav1.ObjectMeta{Name: "blee", Namespace: "ns1"}, + co: "fred", + e: "ns1/blee:fred", + }, + "no-co": { + meta: metav1.ObjectMeta{Name: "blee", Namespace: "ns1"}, + e: "ns1/blee:", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, client.CoFQN(u.meta, u.co)) + }) + } +} + +func TestIsClusterScoped(t *testing.T) { + uu := map[string]struct { + ns string + e bool + }{ + "empty": {}, + "all": { + ns: client.NamespaceAll, + }, + "none": { + ns: client.BlankNamespace, + }, + "custom": { + ns: "fred", + }, + "scoped": { + ns: "-", + e: true, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, client.IsClusterScoped(u.ns)) + }) + } +} + +func TestIsNamespaced(t *testing.T) { + uu := map[string]struct { + ns string + e bool + }{ + "empty": {}, + "all": { + ns: client.NamespaceAll, + }, + "none": { + ns: client.BlankNamespace, + }, + "custom": { + ns: "fred", + e: true, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, client.IsNamespaced(u.ns)) + }) + } +} + +func TestIsAllNamespaces(t *testing.T) { + uu := map[string]struct { + ns string + e bool + }{ + "empty": { + e: true, + }, + "all": { + ns: client.NamespaceAll, + e: true, + }, + "none": { + ns: client.BlankNamespace, + e: true, + }, + "custom": { + ns: "fred", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, client.IsAllNamespaces(u.ns)) + }) + } +} + +func TestIsAllNamespace(t *testing.T) { + uu := map[string]struct { + ns string + e bool + }{ + "empty": {}, + "all": { + ns: client.NamespaceAll, + e: true, + }, + "custom": { + ns: "fred", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, client.IsAllNamespace(u.ns)) + }) + } +} + +func TestCleanseNamespace(t *testing.T) { + uu := map[string]struct { + ns, e string + }{ + "empty": {}, + "all": { + ns: client.NamespaceAll, + e: client.BlankNamespace, + }, + "custom": { + ns: "fred", + e: "fred", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, client.CleanseNamespace(u.ns)) + }) + } +} + func TestNamespaced(t *testing.T) { uu := []struct { p, ns, n string diff --git a/internal/client/helpers.go b/internal/client/helpers.go index 6f45b6040d..204da6a547 100644 --- a/internal/client/helpers.go +++ b/internal/client/helpers.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package client import ( @@ -14,13 +17,13 @@ var toFileName = regexp.MustCompile(`[^(\w/\.)]`) // IsClusterWide returns true if ns designates cluster scope, false otherwise. func IsClusterWide(ns string) bool { - return ns == NamespaceAll || ns == AllNamespaces || ns == ClusterScope + return ns == NamespaceAll || ns == BlankNamespace || ns == ClusterScope } // CleanseNamespace ensures all ns maps to blank. func CleanseNamespace(ns string) string { if IsAllNamespace(ns) { - return AllNamespaces + return BlankNamespace } return ns @@ -33,7 +36,7 @@ func IsAllNamespace(ns string) bool { // IsAllNamespaces returns true if all namespaces, false otherwise. func IsAllNamespaces(ns string) bool { - return ns == NamespaceAll || ns == AllNamespaces + return ns == NamespaceAll || ns == BlankNamespace } // IsNamespaced returns true if a specific ns is given. diff --git a/internal/client/metrics.go b/internal/client/metrics.go index 45aba6ca70..13485c71a4 100644 --- a/internal/client/metrics.go +++ b/internal/client/metrics.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package client import ( @@ -17,6 +20,8 @@ import ( const ( mxCacheSize = 100 mxCacheExpiry = 1 * time.Minute + podMXGVR = "metrics.k8s.io/v1beta1/pods" + nodeMXGVR = "metrics.k8s.io/v1beta1/nodes" ) // MetricsDial tracks global metric server handle. @@ -85,10 +90,10 @@ func (m *MetricsServer) ClusterLoad(nos *v1.NodeList, nmx *mv1beta1.NodeMetricsL func (m *MetricsServer) checkAccess(ns, gvr, msg string) error { if !m.HasMetrics() { - return errors.New("No metrics-server detected on cluster") + return errors.New("no metrics-server detected on cluster") } - auth, err := m.CanI(ns, gvr, ListAccess) + auth, err := m.CanI(ns, gvr, "", ListAccess) if err != nil { return err } @@ -146,7 +151,7 @@ func (m *MetricsServer) FetchNodesMetrics(ctx context.Context) (*mv1beta1.NodeMe const msg = "user is not authorized to list node metrics" mx := new(mv1beta1.NodeMetricsList) - if err := m.checkAccess(ClusterScope, "metrics.k8s.io/v1beta1/nodes", msg); err != nil { + if err := m.checkAccess(ClusterScope, nodeMXGVR, msg); err != nil { return mx, err } @@ -177,7 +182,7 @@ func (m *MetricsServer) FetchNodeMetrics(ctx context.Context, n string) (*mv1bet const msg = "user is not authorized to list node metrics" mx := new(mv1beta1.NodeMetrics) - if err := m.checkAccess(ClusterScope, "metrics.k8s.io/v1beta1/nodes", msg); err != nil { + if err := m.checkAccess(ClusterScope, nodeMXGVR, msg); err != nil { return mx, err } @@ -188,7 +193,7 @@ func (m *MetricsServer) FetchNodeMetrics(ctx context.Context, n string) (*mv1bet mx, ok := mmx[n] if !ok { - return nil, fmt.Errorf("Unable to retrieve node metrics for %q", n) + return nil, fmt.Errorf("unable to retrieve node metrics for %q", n) } return mx, nil } @@ -215,9 +220,9 @@ func (m *MetricsServer) FetchPodsMetrics(ctx context.Context, ns string) (*mv1be const msg = "user is not authorized to list pods metrics" if ns == NamespaceAll { - ns = AllNamespaces + ns = BlankNamespace } - if err := m.checkAccess(ns, "metrics.k8s.io/v1beta1/pods", msg); err != nil { + if err := m.checkAccess(ns, podMXGVR, msg); err != nil { return mx, err } @@ -225,7 +230,7 @@ func (m *MetricsServer) FetchPodsMetrics(ctx context.Context, ns string) (*mv1be if entry, ok := m.cache.Get(key); ok { mxList, ok := entry.(*mv1beta1.PodMetricsList) if !ok { - return mx, fmt.Errorf("expected podmetricslist but got %T", entry) + return mx, fmt.Errorf("expected PodMetricsList but got %T", entry) } return mxList, nil } @@ -266,9 +271,9 @@ func (m *MetricsServer) FetchPodMetrics(ctx context.Context, fqn string) (*mv1be ns, _ := Namespaced(fqn) if ns == NamespaceAll { - ns = AllNamespaces + ns = BlankNamespace } - if err := m.checkAccess(ns, "metrics.k8s.io/v1beta1/pods", msg); err != nil { + if err := m.checkAccess(ns, podMXGVR, msg); err != nil { return mx, err } @@ -278,7 +283,7 @@ func (m *MetricsServer) FetchPodMetrics(ctx context.Context, fqn string) (*mv1be } pmx, ok := mmx[fqn] if !ok { - return nil, fmt.Errorf("Unable to locate pod metrics for pod %q", fqn) + return nil, fmt.Errorf("unable to locate pod metrics for pod %q", fqn) } return pmx, nil diff --git a/internal/client/metrics_test.go b/internal/client/metrics_test.go index 3a632efc80..cb1a357295 100644 --- a/internal/client/metrics_test.go +++ b/internal/client/metrics_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package client_test import ( diff --git a/internal/client/testdata/config b/internal/client/testdata/config index 88e0a0e87f..a43df2c815 100644 --- a/internal/client/testdata/config +++ b/internal/client/testdata/config @@ -13,10 +13,10 @@ clusters: - cluster: insecure-skip-tls-verify: true server: https://localhost:3002 - name: duh + name: zorg contexts: - context: - cluster: fred + cluster: zorg user: fred name: fred - context: diff --git a/internal/client/testdata/config.2 b/internal/client/testdata/config.2 new file mode 100644 index 0000000000..efcf664750 --- /dev/null +++ b/internal/client/testdata/config.2 @@ -0,0 +1,23 @@ +apiVersion: v1 +clusters: +- cluster: + insecure-skip-tls-verify: true + server: https://localhost:3001 + name: blee +- cluster: + insecure-skip-tls-verify: true + server: https://localhost:3002 + name: fred +contexts: +- context: + cluster: blee + user: blee + name: blee +- context: + cluster: fred + user: fred + name: fred +current-context: blee +kind: Config +preferences: {} +users: null diff --git a/internal/client/types.go b/internal/client/types.go index c57dbbdaac..b5a7d3cf76 100644 --- a/internal/client/types.go +++ b/internal/client/types.go @@ -1,7 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package client import ( - v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/version" "k8s.io/client-go/discovery/cached/disk" "k8s.io/client-go/dynamic" @@ -18,8 +20,8 @@ const ( // NamespaceAll designates the fictional all namespace. NamespaceAll = "all" - // AllNamespaces designates all namespaces. - AllNamespaces = "" + // BlankNamespace designates no namespace. + BlankNamespace = "" // DefaultNamespace designates the default namespace. DefaultNamespace = "default" @@ -53,12 +55,18 @@ const ( ) var ( + // PatchAccess patch a resource. + PatchAccess = []string{PatchVerb} + // GetAccess reads a resource. GetAccess = []string{GetVerb} + // ListAccess list resources. ListAccess = []string{ListVerb} + // MonitorAccess monitors a collection of resources. MonitorAccess = []string{ListVerb, WatchVerb} + // ReadAllAccess represents an all read access to a resource. ReadAllAccess = []string{GetVerb, ListVerb, WatchVerb} ) @@ -75,7 +83,7 @@ type PodsMetricsMap map[string]*mv1beta1.PodMetrics // Authorizer checks what a user can or cannot do to a resource. type Authorizer interface { // CanI returns true if the user can use these actions for a given resource. - CanI(ns, gvr string, verbs []string) (bool, error) + CanI(ns, gvr, n string, verbs []string) (bool, error) } // Connection represents a Kubernetes apiserver connection. @@ -112,8 +120,11 @@ type Connection interface { // HasMetrics checks if metrics server is available. HasMetrics() bool - // ValidNamespaces returns all available namespaces. - ValidNamespaces() ([]v1.Namespace, error) + // ValidNamespaceNames returns all available namespace names. + ValidNamespaceNames() (NamespaceNames, error) + + // IsValidNamespace checks if given namespace is known. + IsValidNamespace(string) bool // ServerVersion returns current server version. ServerVersion() (*version.Info, error) @@ -121,8 +132,8 @@ type Connection interface { // CheckConnectivity checks if api server connection is happy or not. CheckConnectivity() bool - // ActiveCluster returns the current cluster name. - ActiveCluster() string + // ActiveContext returns the current context name. + ActiveContext() string // ActiveNamespace returns the current namespace. ActiveNamespace() string diff --git a/internal/color/colorize.go b/internal/color/colorize.go index f4e7177e2e..24811488e2 100644 --- a/internal/color/colorize.go +++ b/internal/color/colorize.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package color import ( diff --git a/internal/color/colorize_test.go b/internal/color/colorize_test.go index db9b768295..3d71f83f7b 100644 --- a/internal/color/colorize_test.go +++ b/internal/color/colorize_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package color_test import ( diff --git a/internal/config/alias.go b/internal/config/alias.go index 8dddf782be..426d41d76f 100644 --- a/internal/config/alias.go +++ b/internal/config/alias.go @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config import ( + "errors" + "fmt" + "io/fs" "os" - "path/filepath" "sync" + "github.com/derailed/k9s/internal/config/data" + "github.com/derailed/k9s/internal/config/json" "github.com/rs/zerolog/log" "gopkg.in/yaml.v2" ) -// K9sAlias manages K9s aliases. -var K9sAlias = filepath.Join(K9sHome(), "alias.yml") - // Alias tracks shortname to GVR mappings. type Alias map[string]string @@ -20,7 +24,7 @@ type ShortNames map[string][]string // Aliases represents a collection of aliases. type Aliases struct { - Alias Alias `yaml:"alias"` + Alias Alias `yaml:"aliases"` mx sync.RWMutex } @@ -31,6 +35,20 @@ func NewAliases() *Aliases { } } +func (a *Aliases) AliasesFor(s string) []string { + aa := make([]string, 0, 10) + + a.mx.RLock() + defer a.mx.RUnlock() + for k, v := range a.Alias { + if v == s { + aa = append(aa, k) + } + } + + return aa +} + // Keys returns all aliases keys. func (a *Aliases) Keys() []string { a.mx.RLock() @@ -98,25 +116,48 @@ func (a *Aliases) Define(gvr string, aliases ...string) { } // Load K9s aliases. -func (a *Aliases) Load() error { +func (a *Aliases) Load(path string) error { a.loadDefaultAliases() - return a.LoadFileAliases(K9sAlias) + + f, err := EnsureAliasesCfgFile() + if err != nil { + log.Error().Err(err).Msgf("Unable to gen config aliases") + } + + // load global alias file + if err := a.LoadFile(f); err != nil { + return err + } + + // load context specific aliases if any + return a.LoadFile(path) } -// LoadFileAliases loads alias from a given file. -func (a *Aliases) LoadFileAliases(path string) error { - f, err := os.ReadFile(path) - if err == nil { - var aa Aliases - if err := yaml.Unmarshal(f, &aa); err != nil { - return err - } +// LoadFile loads alias from a given file. +func (a *Aliases) LoadFile(path string) error { + if path == "" { + return nil + } + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { + return nil + } - a.mx.Lock() - defer a.mx.Unlock() - for k, v := range aa.Alias { - a.Alias[k] = v - } + bb, err := os.ReadFile(path) + if err != nil { + return err + } + if err := data.JSONValidator.Validate(json.AliasesSchema, bb); err != nil { + return fmt.Errorf("validation failed for %q: %w", path, err) + } + + var aa Aliases + if err := yaml.Unmarshal(bb, &aa); err != nil { + return err + } + a.mx.Lock() + defer a.mx.Unlock() + for k, v := range aa.Alias { + a.Alias[k] = v } return nil @@ -133,45 +174,39 @@ func (a *Aliases) loadDefaultAliases() { a.mx.Lock() defer a.mx.Unlock() - a.Alias["dp"] = "apps/v1/deployments" - a.Alias["sec"] = "v1/secrets" - a.Alias["jo"] = "batch/v1/jobs" - a.Alias["cr"] = "rbac.authorization.k8s.io/v1/clusterroles" - a.Alias["crb"] = "rbac.authorization.k8s.io/v1/clusterrolebindings" - a.Alias["ro"] = "rbac.authorization.k8s.io/v1/roles" - a.Alias["rb"] = "rbac.authorization.k8s.io/v1/rolebindings" - a.Alias["np"] = "networking.k8s.io/v1/networkpolicies" - a.declare("help", "h", "?") a.declare("quit", "q", "q!", "qa", "Q") a.declare("aliases", "alias", "a") - a.declare("popeye", "pop") + // !!BOZO!! + // a.declare("popeye", "pop") a.declare("helm", "charts", "chart", "hm") a.declare("dir", "d") a.declare("contexts", "context", "ctx") a.declare("users", "user", "usr") a.declare("groups", "group", "grp") a.declare("portforwards", "portforward", "pf") - a.declare("benchmarks", "bench", "benchmark", "be") + a.declare("benchmarks", "benchmark", "bench") a.declare("screendumps", "screendump", "sd") a.declare("pulses", "pulse", "pu", "hz") a.declare("xrays", "xray", "x") + a.declare("workloads", "workload", "wk") } // Save alias to disk. func (a *Aliases) Save() error { log.Debug().Msg("[Config] Saving Aliases...") - return a.SaveAliases(K9sAlias) + return a.SaveAliases(AppAliasesFile) } // SaveAliases saves aliases to a given file. func (a *Aliases) SaveAliases(path string) error { - if err := EnsureDirPath(path, DefaultDirMod); err != nil { + if err := data.EnsureDirPath(path, data.DefaultDirMod); err != nil { return err } cfg, err := yaml.Marshal(a) if err != nil { return err } - return os.WriteFile(path, cfg, 0644) + + return os.WriteFile(path, cfg, data.DefaultFileMod) } diff --git a/internal/config/alias_test.go b/internal/config/alias_test.go index a627ddd283..c67f4f5824 100644 --- a/internal/config/alias_test.go +++ b/internal/config/alias_test.go @@ -1,25 +1,63 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config_test import ( + "fmt" + "os" + "path" + "slices" "testing" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/data" "github.com/stretchr/testify/assert" ) +func TestAliasClear(t *testing.T) { + a := testAliases() + a.Clear() + + assert.Equal(t, 0, len(a.Keys())) +} + +func TestAliasKeys(t *testing.T) { + a := testAliases() + kk := a.Keys() + slices.Sort(kk) + + assert.Equal(t, []string{"a1", "a11", "a2", "a3"}, kk) +} + +func TestAliasShortNames(t *testing.T) { + a := testAliases() + ess := config.ShortNames{ + "gvr1": []string{"a1", "a11"}, + "gvr2": []string{"a2"}, + "gvr3": []string{"a3"}, + } + ss := a.ShortNames() + assert.Equal(t, len(ess), len(ss)) + for k, v := range ss { + v1, ok := ess[k] + assert.True(t, ok, fmt.Sprintf("missing: %q", k)) + slices.Sort(v) + assert.Equal(t, v1, v) + } +} + func TestAliasDefine(t *testing.T) { type aliasDef struct { cmd string aliases []string } - uu := []struct { - name string + uu := map[string]struct { aliases []aliasDef registeredCommands map[string]string }{ - { - name: "simple aliases", + "simple": { aliases: []aliasDef{ { cmd: "one", @@ -31,8 +69,7 @@ func TestAliasDefine(t *testing.T) { "duh": "one", }, }, - { - name: "duplicated aliases", + "duplicates": { aliases: []aliasDef{ { cmd: "one", @@ -51,9 +88,9 @@ func TestAliasDefine(t *testing.T) { }, } - for i := range uu { - u := uu[i] - t.Run(u.name, func(t *testing.T) { + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { configAlias := config.NewAliases() for _, aliases := range u.aliases { for _, a := range aliases.aliases { @@ -70,18 +107,35 @@ func TestAliasDefine(t *testing.T) { } func TestAliasesLoad(t *testing.T) { + config.AppConfigDir = "testdata/aliases" a := config.NewAliases() - assert.Nil(t, a.LoadFileAliases("testdata/alias.yml")) - assert.Equal(t, 2, len(a.Alias)) + assert.Nil(t, a.Load(path.Join(config.AppConfigDir, "plain.yaml"))) + assert.Equal(t, 54, len(a.Alias)) } func TestAliasesSave(t *testing.T) { + assert.NoError(t, data.EnsureFullPath("/tmp/test-aliases", data.DefaultDirMod)) + defer assert.NoError(t, os.RemoveAll("/tmp/test-aliases")) + + config.AppAliasesFile = "/tmp/test-aliases/aliases.yaml" + a := testAliases() + c := len(a.Alias) + + assert.Equal(t, c, len(a.Alias)) + assert.Nil(t, a.Save()) + assert.Nil(t, a.LoadFile(config.AppAliasesFile)) + assert.Equal(t, c, len(a.Alias)) +} + +// Helpers... + +func testAliases() *config.Aliases { a := config.NewAliases() - a.Alias["test"] = "fred" - a.Alias["blee"] = "duh" + a.Alias["a1"] = "gvr1" + a.Alias["a11"] = "gvr1" + a.Alias["a2"] = "gvr2" + a.Alias["a3"] = "gvr3" - assert.Nil(t, a.SaveAliases("/tmp/a.yml")) - assert.Nil(t, a.LoadFileAliases("/tmp/a.yml")) - assert.Equal(t, 2, len(a.Alias)) + return a } diff --git a/internal/config/bench.go b/internal/config/benchmark.go similarity index 97% rename from internal/config/bench.go rename to internal/config/benchmark.go index c3f6c4c95b..329c094011 100644 --- a/internal/config/bench.go +++ b/internal/config/benchmark.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config import ( @@ -64,6 +67,18 @@ const ( DefaultMethod = "GET" ) +// DefaultBenchSpec returns a default bench spec. +func DefaultBenchSpec() BenchConfig { + return BenchConfig{ + C: DefaultC, + N: DefaultN, + HTTP: HTTP{ + Method: DefaultMethod, + Path: "/", + }, + } +} + func newBenchmark() Benchmark { return Benchmark{ C: DefaultC, @@ -103,15 +118,3 @@ func (s *Bench) load(path string) error { return yaml.Unmarshal(f, &s) } - -// DefaultBenchSpec returns a default bench spec. -func DefaultBenchSpec() BenchConfig { - return BenchConfig{ - C: DefaultC, - N: DefaultN, - HTTP: HTTP{ - Method: DefaultMethod, - Path: "/", - }, - } -} diff --git a/internal/config/bench_test.go b/internal/config/benchmark_test.go similarity index 89% rename from internal/config/bench_test.go rename to internal/config/benchmark_test.go index fd6e4d97aa..cd80d44220 100644 --- a/internal/config/bench_test.go +++ b/internal/config/benchmark_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config import ( @@ -32,14 +35,14 @@ func TestBenchLoad(t *testing.T) { coCount int }{ "goodConfig": { - "testdata/b_good.yml", + "testdata/benchmarks/b_good.yaml", 2, 1000, 2, 0, }, "malformed": { - "testdata/b_toast.yml", + "testdata/benchmarks/b_toast.yaml", 1, 200, 0, @@ -100,7 +103,7 @@ func TestBenchServiceLoad(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - b, err := NewBench("testdata/b_good.yml") + b, err := NewBench("testdata/benchmarks/b_good.yaml") assert.Nil(t, err) assert.Equal(t, 2, len(b.Benchmarks.Services)) @@ -119,16 +122,16 @@ func TestBenchServiceLoad(t *testing.T) { } func TestBenchReLoad(t *testing.T) { - b, err := NewBench("testdata/b_containers.yml") + b, err := NewBench("testdata/benchmarks/b_containers.yaml") assert.Nil(t, err) assert.Equal(t, 2, b.Benchmarks.Defaults.C) - assert.Nil(t, b.Reload("testdata/b_containers_1.yml")) + assert.NoError(t, b.Reload("testdata/benchmarks/b_containers_1.yaml")) assert.Equal(t, 20, b.Benchmarks.Defaults.C) } func TestBenchLoadToast(t *testing.T) { - _, err := NewBench("testdata/toast.yml") + _, err := NewBench("testdata/toast.yaml") assert.NotNil(t, err) } @@ -171,7 +174,7 @@ func TestBenchContainerLoad(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - b, err := NewBench("testdata/b_containers.yml") + b, err := NewBench("testdata/benchmarks/b_containers.yaml") assert.Nil(t, err) assert.Equal(t, 2, len(b.Benchmarks.Services)) diff --git a/internal/config/cluster.go b/internal/config/cluster.go deleted file mode 100644 index ec0a9c3274..0000000000 --- a/internal/config/cluster.go +++ /dev/null @@ -1,54 +0,0 @@ -package config - -import "github.com/derailed/k9s/internal/client" - -// DefaultPFAddress specifies the default PortForward host address. -const DefaultPFAddress = "localhost" - -// Cluster tracks K9s cluster configuration. -type Cluster struct { - Namespace *Namespace `yaml:"namespace"` - View *View `yaml:"view"` - FeatureGates *FeatureGates `yaml:"featureGates"` - ShellPod *ShellPod `yaml:"shellPod"` - PortForwardAddress string `yaml:"portForwardAddress"` -} - -// NewCluster creates a new cluster configuration. -func NewCluster() *Cluster { - return &Cluster{ - Namespace: NewNamespace(), - View: NewView(), - PortForwardAddress: DefaultPFAddress, - FeatureGates: NewFeatureGates(), - ShellPod: NewShellPod(), - } -} - -// Validate a cluster config. -func (c *Cluster) Validate(conn client.Connection, ks KubeSettings) { - if c.PortForwardAddress == "" { - c.PortForwardAddress = DefaultPFAddress - } - - if c.Namespace == nil { - c.Namespace = NewNamespace() - } - if c.Namespace.Active == client.AllNamespaces { - c.Namespace.Active = client.NamespaceAll - } - - if c.FeatureGates == nil { - c.FeatureGates = NewFeatureGates() - } - - if c.View == nil { - c.View = NewView() - } - c.View.Validate() - - if c.ShellPod == nil { - c.ShellPod = NewShellPod() - } - c.ShellPod.Validate(conn, ks) -} diff --git a/internal/config/cluster_test.go b/internal/config/cluster_test.go deleted file mode 100644 index 53e3c0565e..0000000000 --- a/internal/config/cluster_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package config_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/config" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestClusterValidate(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - - mk := NewMockKubeSettings() - m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"ns1", "ns2", "default"}) - - c := config.NewCluster() - c.Validate(mc, mk) - - assert.Equal(t, "po", c.View.Active) - assert.Equal(t, "default", c.Namespace.Active) - assert.Equal(t, 1, len(c.Namespace.Favorites)) - assert.Equal(t, []string{"default"}, c.Namespace.Favorites) -} - -func TestClusterValidateEmpty(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - - mk := NewMockKubeSettings() - m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"ns1", "ns2", "default"}) - - var c config.Cluster - c.Validate(mc, mk) - - assert.Equal(t, "po", c.View.Active) - assert.Equal(t, "default", c.Namespace.Active) - assert.Equal(t, 1, len(c.Namespace.Favorites)) - assert.Equal(t, []string{"default"}, c.Namespace.Favorites) -} - -func namespaces() []v1.Namespace { - return []v1.Namespace{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "default", - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "fred", - }, - }, - } -} diff --git a/internal/config/color.go b/internal/config/color.go new file mode 100644 index 0000000000..17fb595f94 --- /dev/null +++ b/internal/config/color.go @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package config + +import ( + "fmt" + + "github.com/derailed/tcell/v2" +) + +const ( + // DefaultColor represents a default color. + DefaultColor Color = "default" + + // TransparentColor represents the terminal bg color. + TransparentColor Color = "-" +) + +// Colors tracks multiple colors. +type Colors []Color + +// Colors converts series string colors to colors. +func (c Colors) Colors() []tcell.Color { + cc := make([]tcell.Color, 0, len(c)) + for _, color := range c { + cc = append(cc, color.Color()) + } + + return cc +} + +// Color represents a color. +type Color string + +// NewColor returns a new color. +func NewColor(c string) Color { + return Color(c) +} + +// String returns color as string. +func (c Color) String() string { + if c.isHex() { + return string(c) + } + if c == DefaultColor { + return "-" + } + col := c.Color().TrueColor().Hex() + if col < 0 { + return "-" + } + + return fmt.Sprintf("#%06x", col) +} + +func (c Color) isHex() bool { + return len(c) == 7 && c[0] == '#' +} + +// Color returns a view color. +func (c Color) Color() tcell.Color { + if c == DefaultColor { + return tcell.ColorDefault + } + + return tcell.GetColor(string(c)).TrueColor() +} diff --git a/internal/config/color_test.go b/internal/config/color_test.go new file mode 100644 index 0000000000..09d41ae82e --- /dev/null +++ b/internal/config/color_test.go @@ -0,0 +1,119 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package config_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/tcell/v2" + "github.com/stretchr/testify/assert" +) + +func TestColors(t *testing.T) { + uu := map[string]struct { + cc []string + ee []tcell.Color + }{ + "empty": { + ee: []tcell.Color{}, + }, + "default": { + cc: []string{"default"}, + ee: []tcell.Color{tcell.ColorDefault}, + }, + "multi": { + cc: []string{ + "default", + "transparent", + "blue", + "green", + }, + ee: []tcell.Color{ + tcell.ColorDefault, + tcell.ColorDefault, + tcell.ColorBlue.TrueColor(), + tcell.ColorGreen.TrueColor(), + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + cc := make(config.Colors, 0, len(u.cc)) + for _, c := range u.cc { + cc = append(cc, config.NewColor(c)) + } + assert.Equal(t, u.ee, cc.Colors()) + }) + } +} + +func TestColorString(t *testing.T) { + uu := map[string]struct { + c string + e string + }{ + "empty": { + e: "-", + }, + "default": { + c: "default", + e: "-", + }, + "transparent": { + c: "-", + e: "-", + }, + "blue": { + c: "blue", + e: "#0000ff", + }, + "lightgray": { + c: "lightgray", + e: "#d3d3d3", + }, + "hex": { + c: "#00ff00", + e: "#00ff00", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + c := config.NewColor(u.c) + assert.Equal(t, u.e, c.String()) + }) + } +} + +func TestColorToColor(t *testing.T) { + uu := map[string]struct { + c string + e tcell.Color + }{ + "default": { + c: "default", + e: tcell.ColorDefault, + }, + "transparent": { + c: "-", + e: tcell.ColorDefault, + }, + "aqua": { + c: "aqua", + e: tcell.ColorAqua.TrueColor(), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + c := config.NewColor(u.c) + assert.Equal(t, u.e, c.Color()) + }) + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 3a10544cfa..a5da1dfa89 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,194 +1,176 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config import ( "errors" "fmt" + "io/fs" "os" - "path/filepath" - "github.com/adrg/xdg" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config/data" + "github.com/derailed/k9s/internal/config/json" + "github.com/derailed/k9s/internal/view/cmd" "github.com/rs/zerolog/log" "gopkg.in/yaml.v2" "k8s.io/cli-runtime/pkg/genericclioptions" ) -// K9sConfig represents K9s configuration dir env var. -const K9sConfig = "K9SCONFIG" - -var ( - // K9sConfigFile represents K9s config file location. - K9sConfigFile = filepath.Join(K9sHome(), "config.yml") - // K9sDefaultScreenDumpDir represents a default directory where K9s screen dumps will be persisted. - K9sDefaultScreenDumpDir = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-screens-%s", MustK9sUser())) -) - -type ( - // KubeSettings exposes kubeconfig context information. - KubeSettings interface { - // CurrentContextName returns the name of the current context. - CurrentContextName() (string, error) - - // CurrentClusterName returns the name of the current cluster. - CurrentClusterName() (string, error) - - // CurrentNamespace returns the name of the current namespace. - CurrentNamespaceName() (string, error) +// Config tracks K9s configuration options. +type Config struct { + K9s *K9s `yaml:"k9s" json:"k9s"` + conn client.Connection + settings data.KubeSettings +} - // ClusterNames() returns all available cluster names. - ClusterNames() (map[string]struct{}, error) +// NewConfig creates a new default config. +func NewConfig(ks data.KubeSettings) *Config { + return &Config{ + settings: ks, + K9s: NewK9s(nil, ks), } +} - // Config tracks K9s configuration options. - Config struct { - K9s *K9s `yaml:"k9s"` - client client.Connection - settings KubeSettings +// ContextHotkeysPath returns a context specific hotkeys file spec. +func (c *Config) ContextHotkeysPath() string { + ct, err := c.K9s.ActiveContext() + if err != nil { + return "" } -) -// K9sHome returns k9s configs home directory. -func K9sHome() string { - if env := os.Getenv(K9sConfig); env != "" { - return env - } + return AppContextHotkeysFile(ct.ClusterName, c.K9s.activeContextName) +} - xdgK9sHome, err := xdg.ConfigFile("k9s") +// ContextAliasesPath returns a context specific aliases file spec. +func (c *Config) ContextAliasesPath() string { + ct, err := c.K9s.ActiveContext() if err != nil { - log.Fatal().Err(err).Msg("Unable to create configuration directory for k9s") + return "" } - return xdgK9sHome + return AppContextAliasesFile(ct.GetClusterName(), c.K9s.activeContextName) } -// NewConfig creates a new default config. -func NewConfig(ks KubeSettings) *Config { - return &Config{K9s: NewK9s(), settings: ks} +// ContextPluginsPath returns a context specific plugins file spec. +func (c *Config) ContextPluginsPath() (string, error) { + ct, err := c.K9s.ActiveContext() + if err != nil { + return "", err + } + + return AppContextPluginsFile(ct.GetClusterName(), c.K9s.activeContextName), nil } // Refine the configuration based on cli args. func (c *Config) Refine(flags *genericclioptions.ConfigFlags, k9sFlags *Flags, cfg *client.Config) error { - if isSet(flags.Context) { - c.K9s.CurrentContext = *flags.Context + if flags == nil { + return nil + } + if isStringSet(flags.Context) { + if _, err := c.K9s.ActivateContext(*flags.Context); err != nil { + return fmt.Errorf("k8sflags. unable to activate context %q: %w", *flags.Context, err) + } } else { - context, err := cfg.CurrentContextName() + n, err := cfg.CurrentContextName() if err != nil { - return err + return fmt.Errorf("unable to retrieve kubeconfig current context %q: %w", n, err) + } + _, err = c.K9s.ActivateContext(n) + if err != nil { + return fmt.Errorf("unable to activate context %q: %w", n, err) } - c.K9s.CurrentContext = context - } - log.Debug().Msgf("Active Context %q", c.K9s.CurrentContext) - if c.K9s.CurrentContext == "" { - return errors.New("Invalid kubeconfig context detected") - } - cc, err := cfg.Contexts() - if err != nil { - return err - } - context, ok := cc[c.K9s.CurrentContext] - if !ok { - return fmt.Errorf("the specified context %q does not exists in kubeconfig", c.K9s.CurrentContext) } - c.K9s.CurrentCluster = context.Cluster - c.K9s.ActivateCluster(context.Namespace) + log.Debug().Msgf("Active Context %q", c.K9s.ActiveContextName()) - var ns = client.DefaultNamespace + var ns string switch { case k9sFlags != nil && IsBoolSet(k9sFlags.AllNamespaces): ns = client.NamespaceAll - case isSet(flags.Namespace): + c.ResetActiveView() + case isStringSet(flags.Namespace): ns = *flags.Namespace default: - if nss := context.Namespace; nss != "" { - ns = nss - } else if nss == "" { - ns = c.K9s.ActiveCluster().Namespace.Active + nss, err := c.K9s.ActiveContextNamespace() + if err != nil { + return err } + ns = nss + } + if ns == "" { + ns = client.DefaultNamespace } - if err := c.SetActiveNamespace(ns); err != nil { return err } - flags.Namespace = &ns - - if isSet(flags.ClusterName) { - c.K9s.CurrentCluster = *flags.ClusterName - } - return EnsureDirPath(c.K9s.GetScreenDumpDir(), DefaultDirMod) + return data.EnsureDirPath(c.K9s.AppScreenDumpDir(), data.DefaultDirMod) } -// Reset the context to the new current context/cluster. -// if it does not exist. +// Reset resets the context to the new current context/cluster. func (c *Config) Reset() { - c.K9s.CurrentContext, c.K9s.CurrentCluster = "", "" + c.K9s.Reset() } -// CurrentCluster fetch the configuration activeCluster. -func (c *Config) CurrentCluster() *Cluster { - if c, ok := c.K9s.Clusters[c.K9s.CurrentCluster]; ok { - return c +func (c *Config) SetCurrentContext(n string) (*data.Context, error) { + ct, err := c.K9s.ActivateContext(n) + if err != nil { + return nil, fmt.Errorf("set current context failed. %w", err) } - return nil + + return ct, nil +} + +// CurrentContext fetch the configuration active context. +func (c *Config) CurrentContext() (*data.Context, error) { + return c.K9s.ActiveContext() } -// ActiveNamespace returns the active namespace in the current cluster. +// ActiveNamespace returns the active namespace in the current context. +// If none found return the empty ns. func (c *Config) ActiveNamespace() string { - if c.K9s.Clusters == nil { - log.Warn().Msgf("No context detected returning default namespace") - return "default" - } - cl := c.CurrentCluster() - if cl != nil && cl.Namespace != nil { - return cl.Namespace.Active - } - if cl == nil { - cl = NewCluster() - c.K9s.Clusters[c.K9s.CurrentCluster] = cl - } - if ns, err := c.settings.CurrentNamespaceName(); err == nil && ns != "" { - if cl.Namespace == nil { - cl.Namespace = NewNamespace() - } - cl.Namespace.Active = ns - return ns + ns, err := c.K9s.ActiveContextNamespace() + if err != nil { + log.Error().Err(err).Msgf("Unable to assert active namespace. Using default") + ns = client.DefaultNamespace } - return "default" -} - -// ValidateFavorites ensure favorite ns are legit. -func (c *Config) ValidateFavorites() { - cl := c.K9s.ActiveCluster() - cl.Validate(c.client, c.settings) - cl.Namespace.Validate(c.client, c.settings) + return ns } -// FavNamespaces returns fav namespaces in the current cluster. +// FavNamespaces returns fav namespaces in the current context. func (c *Config) FavNamespaces() []string { - cl := c.K9s.ActiveCluster() + ct, err := c.K9s.ActiveContext() + if err != nil { + return nil + } + ct.Validate(c.conn, c.settings) - return cl.Namespace.Favorites + return ct.Namespace.Favorites } -// SetActiveNamespace set the active namespace in the current cluster. +// SetActiveNamespace set the active namespace in the current context. func (c *Config) SetActiveNamespace(ns string) error { - if cl := c.K9s.ActiveCluster(); cl != nil { - return cl.Namespace.SetActive(ns, c.settings) + if ns == client.NotNamespaced { + log.Debug().Msgf("[SetNS] No namespace given. skipping!") + return nil + } + ct, err := c.K9s.ActiveContext() + if err != nil { + return err } - err := errors.New("no active cluster. unable to set active namespace") - log.Error().Err(err).Msg("SetActiveNamespace") - return err + return ct.Namespace.SetActive(ns, c.settings) } -// ActiveView returns the active view in the current cluster. +// ActiveView returns the active view in the current context. func (c *Config) ActiveView() string { - cl := c.K9s.ActiveCluster() - if cl == nil { - return defaultView + ct, err := c.K9s.ActiveContext() + if err != nil { + return data.DefaultView } - cmd := cl.View.Active + cmd := ct.View.Active if c.K9s.manualCommand != nil && *c.K9s.manualCommand != "" { cmd = *c.K9s.manualCommand // We reset the manualCommand property because @@ -200,54 +182,89 @@ func (c *Config) ActiveView() string { return cmd } -// SetActiveView set the currently cluster active view. +func (c *Config) ResetActiveView() { + if isStringSet(c.K9s.manualCommand) { + return + } + v := c.ActiveView() + if v == "" { + return + } + p := cmd.NewInterpreter(v) + if p.HasNS() { + c.SetActiveView(p.Cmd()) + } +} + +// SetActiveView sets current context active view. func (c *Config) SetActiveView(view string) { - if cl := c.K9s.ActiveCluster(); cl != nil { - cl.View.Active = view + if ct, err := c.K9s.ActiveContext(); err == nil { + ct.View.Active = view } } // GetConnection return an api server connection. func (c *Config) GetConnection() client.Connection { - return c.client + return c.conn } // SetConnection set an api server connection. func (c *Config) SetConnection(conn client.Connection) { - c.client = conn + c.conn = conn + if conn != nil { + c.K9s.resetConnection(conn) + } +} + +func (c *Config) ActiveContextName() string { + return c.K9s.activeContextName +} + +func (c *Config) Merge(c1 *Config) { + c.K9s.Merge(c1.K9s) } -// Load K9s configuration from file. -func (c *Config) Load(path string) error { - f, err := os.ReadFile(path) +// Load loads K9s configuration from file. +func (c *Config) Load(path string, force bool) error { + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { + if err := c.Save(force); err != nil { + return err + } + } + bb, err := os.ReadFile(path) if err != nil { return err } - c.K9s = NewK9s() + var errs error + if err := data.JSONValidator.Validate(json.K9sSchema, bb); err != nil { + errs = errors.Join(errs, fmt.Errorf("k9s config file %q load failed:\n%w", path, err)) + } var cfg Config - if err := yaml.Unmarshal(f, &cfg); err != nil { - return err - } - if cfg.K9s != nil { - c.K9s = cfg.K9s - } - if c.K9s.Logger == nil { - c.K9s.Logger = NewLogger() + if err := yaml.Unmarshal(bb, &cfg); err != nil { + errs = errors.Join(errs, fmt.Errorf("main config.yaml load failed: %w", err)) } - return nil + c.Merge(&cfg) + + return errs } // Save configuration to disk. -func (c *Config) Save() error { +func (c *Config) Save(force bool) error { c.Validate() + if err := c.K9s.Save(force); err != nil { + return err + } + if _, err := os.Stat(AppConfigFile); errors.Is(err, fs.ErrNotExist) { + return c.SaveFile(AppConfigFile) + } - return c.SaveFile(K9sConfigFile) + return nil } // SaveFile K9s configuration to disk. func (c *Config) SaveFile(path string) error { - if err := EnsureDirPath(path, DefaultDirMod); err != nil { + if err := data.EnsureDirPath(path, data.DefaultDirMod); err != nil { return err } cfg, err := yaml.Marshal(c) @@ -255,25 +272,25 @@ func (c *Config) SaveFile(path string) error { log.Error().Msgf("[Config] Unable to save K9s config file: %v", err) return err } - return os.WriteFile(path, cfg, 0644) + + return os.WriteFile(path, cfg, data.DefaultFileMod) } // Validate the configuration. func (c *Config) Validate() { - c.K9s.Validate(c.client, c.settings) + if c.K9s == nil { + c.K9s = NewK9s(c.conn, c.settings) + } + c.K9s.Validate(c.conn, c.settings) } -// Dump debug... +// Dump for debug... func (c *Config) Dump(msg string) { - log.Debug().Msgf("Current Cluster: %s\n", c.K9s.CurrentCluster) - for k, cl := range c.K9s.Clusters { - log.Debug().Msgf("K9s cluster: %s -- %+v\n", k, cl.Namespace) + ct, err := c.K9s.ActiveContext() + if err == nil { + bb, _ := yaml.Marshal(ct) + fmt.Printf("Dump: %q\n%s\n", msg, string(bb)) + } else { + fmt.Println("BOOM!", err) } } - -// ---------------------------------------------------------------------------- -// Helpers... - -func isSet(s *string) bool { - return s != nil && len(*s) > 0 -} diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 149ecdaf41..3910eb5c74 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1,13 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config_test import ( + "errors" "fmt" "os" "path/filepath" "testing" + "github.com/adrg/xdg" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/data" + "github.com/derailed/k9s/internal/config/mock" m "github.com/petergtz/pegomock" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" @@ -18,232 +25,562 @@ func init() { zerolog.SetGlobalLevel(zerolog.FatalLevel) } -func TestConfigRefine(t *testing.T) { - cfgFile, ctx, cluster, ns := "testdata/kubeconfig-test.yml", "test2", "cluster2", "ns2" +func TestConfigSave(t *testing.T) { + config.AppConfigFile = "/tmp/k9s-test/k9s.yaml" + sd := "/tmp/k9s-test/screen-dumps" + cl, ct := "cl-1", "ct-1-1" + _ = os.RemoveAll(("/tmp/k9s-test")) + uu := map[string]struct { - flags *genericclioptions.ConfigFlags - issue bool - context, cluster, namespace string + ct string + flags *genericclioptions.ConfigFlags + k9sFlags *config.Flags }{ - "plain": { - flags: &genericclioptions.ConfigFlags{KubeConfig: &cfgFile}, - issue: false, - context: "test1", - cluster: "cluster1", - namespace: "ns1", - }, - "overrideNS": { + "happy": { + ct: "ct-1-1", flags: &genericclioptions.ConfigFlags{ - KubeConfig: &cfgFile, - Context: &ctx, - ClusterName: &cluster, - Namespace: &ns, + ClusterName: &cl, + Context: &ct, }, - issue: false, - context: ctx, - cluster: cluster, - namespace: ns, + k9sFlags: &config.Flags{ + ScreenDumpDir: &sd, + }, + }, + } + + for k := range uu { + xdg.Reload() + u := uu[k] + t.Run(k, func(t *testing.T) { + c := mock.NewMockConfig() + _, err := c.K9s.ActivateContext(u.ct) + assert.NoError(t, err) + if u.flags != nil { + c.K9s.Override(u.k9sFlags) + assert.NoError(t, c.Refine(u.flags, u.k9sFlags, client.NewConfig(u.flags))) + } + assert.NoError(t, c.Save(true)) + bb, err := os.ReadFile(config.AppConfigFile) + assert.NoError(t, err) + ee, err := os.ReadFile("testdata/configs/default.yaml") + assert.NoError(t, err) + assert.Equal(t, string(ee), string(bb)) + }) + } +} + +func TestSetActiveView(t *testing.T) { + var ( + cfgFile = "testdata/kubes/test.yaml" + view = "dp" + ) + + uu := map[string]struct { + ct string + flags *genericclioptions.ConfigFlags + k9sFlags *config.Flags + view string + e string + }{ + "empty": { + view: data.DefaultView, + e: data.DefaultView, + }, + "not-exists": { + ct: "fred", + view: data.DefaultView, + e: data.DefaultView, }, - "badContext": { + "happy": { + ct: "ct-1-1", + view: "xray", + e: "xray", + }, + "cli-override": { flags: &genericclioptions.ConfigFlags{ - KubeConfig: &cfgFile, - Context: &ns, - ClusterName: &cluster, - Namespace: &ns, + KubeConfig: &cfgFile, + }, + k9sFlags: &config.Flags{ + Command: &view, }, - issue: true, + ct: "ct-1-1", + view: "xray", + e: "dp", }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - mk := newMockSettings(u.flags) - cfg := config.NewConfig(mk) - - err := cfg.Refine(u.flags, nil, client.NewConfig(u.flags)) - if u.issue { - assert.NotNil(t, err) - } else { - assert.Nil(t, err) - assert.Equal(t, u.context, cfg.K9s.CurrentContext) - assert.Equal(t, u.cluster, cfg.K9s.CurrentCluster) - assert.Equal(t, u.namespace, cfg.ActiveNamespace()) + c := mock.NewMockConfig() + _, _ = c.K9s.ActivateContext(u.ct) + if u.flags != nil { + assert.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags))) + c.K9s.Override(u.k9sFlags) } + c.SetActiveView(u.view) + assert.Equal(t, u.e, c.ActiveView()) }) } } -func TestConfigValidate(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) +func TestActiveContextName(t *testing.T) { + var ( + cfgFile = "testdata/kubes/test.yaml" + ct2 = "ct-1-2" + ) - mk := NewMockKubeSettings() - m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"default"}) + uu := map[string]struct { + flags *genericclioptions.ConfigFlags + k9sFlags *config.Flags + ct string + e string + }{ + "empty": {}, + "happy": { + ct: "ct-1-1", + e: "ct-1-1", + }, + "cli-override": { + flags: &genericclioptions.ConfigFlags{ + KubeConfig: &cfgFile, + Context: &ct2, + }, + k9sFlags: &config.Flags{}, + ct: "ct-1-1", + e: "ct-1-2", + }, + } - cfg := config.NewConfig(mk) - cfg.SetConnection(mc) - assert.Nil(t, cfg.Load("testdata/k9s.yml")) - cfg.Validate() + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + c := mock.NewMockConfig() + _, _ = c.K9s.ActivateContext(u.ct) + if u.flags != nil { + assert.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags))) + c.K9s.Override(u.k9sFlags) + } + assert.Equal(t, u.e, c.ActiveContextName()) + }) + } } -func TestConfigLoad(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("testdata/k9s.yml")) +func TestActiveView(t *testing.T) { + var ( + cfgFile = "testdata/kubes/test.yaml" + view = "dp" + ) - assert.Equal(t, 2, cfg.K9s.RefreshRate) - assert.Equal(t, 2000, cfg.K9s.Logger.BufferSize) - assert.Equal(t, int64(200), cfg.K9s.Logger.TailCount) - assert.Equal(t, "minikube", cfg.K9s.CurrentContext) - assert.Equal(t, "minikube", cfg.K9s.CurrentCluster) - assert.NotNil(t, cfg.K9s.Clusters) - assert.Equal(t, 2, len(cfg.K9s.Clusters)) - - nn := []string{ - "default", - "kube-public", - "istio-system", - "all", - "kube-system", + uu := map[string]struct { + ct string + flags *genericclioptions.ConfigFlags + k9sFlags *config.Flags + e string + }{ + "empty": { + e: data.DefaultView, + }, + "not-exists": { + ct: "fred", + e: data.DefaultView, + }, + "happy": { + ct: "ct-1-1", + e: data.DefaultView, + }, + "cli-override": { + flags: &genericclioptions.ConfigFlags{ + KubeConfig: &cfgFile, + }, + k9sFlags: &config.Flags{ + Command: &view, + }, + e: "dp", + }, } - assert.Equal(t, "kube-system", cfg.K9s.Clusters["minikube"].Namespace.Active) - assert.Equal(t, nn, cfg.K9s.Clusters["minikube"].Namespace.Favorites) - assert.Equal(t, "ctx", cfg.K9s.Clusters["minikube"].View.Active) + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + c := mock.NewMockConfig() + _, _ = c.K9s.ActivateContext(u.ct) + if u.flags != nil { + assert.NoError(t, c.Refine(u.flags, nil, client.NewConfig(u.flags))) + c.K9s.Override(u.k9sFlags) + } + assert.Equal(t, u.e, c.ActiveView()) + }) + } } -func TestConfigCurrentCluster(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) +func TestFavNamespaces(t *testing.T) { + uu := map[string]struct { + ct string + e []string + }{ + "empty": {}, + "not-exists": { + ct: "fred", + }, + "happy": { + ct: "ct-1-1", + e: []string{client.DefaultNamespace}, + }, + } - assert.Nil(t, cfg.Load("testdata/k9s.yml")) - assert.NotNil(t, cfg.CurrentCluster()) - assert.Equal(t, "kube-system", cfg.CurrentCluster().Namespace.Active) - assert.Equal(t, "ctx", cfg.CurrentCluster().View.Active) + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + c := mock.NewMockConfig() + _, _ = c.K9s.ActivateContext(u.ct) + assert.Equal(t, u.e, c.FavNamespaces()) + }) + } } -func TestConfigActiveNamespace(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) +func TestContextAliasesPath(t *testing.T) { + uu := map[string]struct { + ct string + e string + }{ + "empty": {}, + "not-exists": { + ct: "fred", + }, + "happy": { + ct: "ct-1-1", + e: "/tmp/test/cl-1/ct-1-1/aliases.yaml", + }, + } - assert.Nil(t, cfg.Load("testdata/k9s.yml")) - assert.Equal(t, "kube-system", cfg.ActiveNamespace()) + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + c := mock.NewMockConfig() + _, _ = c.K9s.ActivateContext(u.ct) + assert.Equal(t, u.e, c.ContextAliasesPath()) + }) + } } -func TestConfigActiveNamespaceBlank(t *testing.T) { - cfg := config.Config{K9s: new(config.K9s)} - assert.Equal(t, "default", cfg.ActiveNamespace()) +func TestContextPluginsPath(t *testing.T) { + uu := map[string]struct { + ct, e string + err error + }{ + "empty": { + err: errors.New(`no context found for: ""`), + }, + "happy": { + ct: "ct-1-1", + e: "/tmp/test/cl-1/ct-1-1/plugins.yaml", + }, + "not-exists": { + ct: "fred", + err: errors.New(`no context found for: "fred"`), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + c := mock.NewMockConfig() + _, _ = c.K9s.ActivateContext(u.ct) + s, err := c.ContextPluginsPath() + if err != nil { + assert.Equal(t, u.err, err) + } + assert.Equal(t, u.e, s) + }) + } } -func TestConfigSetActiveNamespace(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) +func TestConfigLoader(t *testing.T) { + uu := map[string]struct { + f string + err string + }{ + "happy": { + f: "testdata/configs/k9s.yaml", + }, + "toast": { + f: "testdata/configs/k9s_toast.yaml", + err: `k9s config file "testdata/configs/k9s_toast.yaml" load failed: +Additional property disablePodCounts is not allowed +Additional property shellPods is not allowed +Invalid type. Expected: boolean, given: string`, + }, + } - assert.Nil(t, cfg.Load("testdata/k9s.yml")) - assert.Nil(t, cfg.SetActiveNamespace("default")) - assert.Equal(t, "default", cfg.ActiveNamespace()) + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + cfg := config.NewConfig(nil) + if err := cfg.Load(u.f, true); err != nil { + assert.Equal(t, u.err, err.Error()) + } + }) + } } -func TestConfigActiveView(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) +func TestConfigSetCurrentContext(t *testing.T) { + uu := map[string]struct { + cl, ct string + err string + }{ + "happy": { + ct: "ct-1-2", + cl: "cl-1", + }, + "toast": { + ct: "fred", + cl: "cl-1", + err: `set current context failed. no context found for: "fred"`, + }, + } - assert.Nil(t, cfg.Load("testdata/k9s.yml")) - assert.Equal(t, "ctx", cfg.ActiveView()) + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + cfg := mock.NewMockConfig() + ct, err := cfg.SetCurrentContext(u.ct) + if err != nil { + assert.Equal(t, u.err, err.Error()) + return + } + assert.NoError(t, err) + assert.Equal(t, u.cl, ct.ClusterName) + }) + } } -func TestConfigActiveViewBlank(t *testing.T) { - cfg := config.Config{K9s: new(config.K9s)} - assert.Equal(t, "po", cfg.ActiveView()) +func TestConfigCurrentContext(t *testing.T) { + var ( + cfgFile = "testdata/kubes/test.yaml" + ct2 = "ct-1-2" + ) + + uu := map[string]struct { + flags *genericclioptions.ConfigFlags + err error + context string + cluster string + namespace string + }{ + "override-context": { + flags: &genericclioptions.ConfigFlags{ + KubeConfig: &cfgFile, + Context: &ct2, + }, + cluster: "cl-1", + context: "ct-1-2", + namespace: "ns-2", + }, + "use-current-context": { + flags: &genericclioptions.ConfigFlags{ + KubeConfig: &cfgFile, + }, + cluster: "cl-1", + context: "ct-1-1", + namespace: client.DefaultNamespace, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + cfg := mock.NewMockConfig() + + err := cfg.Refine(u.flags, nil, client.NewConfig(u.flags)) + assert.NoError(t, err) + ct, err := cfg.CurrentContext() + assert.NoError(t, err) + assert.Equal(t, u.cluster, ct.ClusterName) + assert.Equal(t, u.namespace, ct.Namespace.Active) + }) + } } -func TestConfigSetActiveView(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) +func TestConfigRefine(t *testing.T) { + var ( + cfgFile = "testdata/kubes/test.yaml" + cl1 = "cl-1" + ct2 = "ct-1-2" + ns1, ns2, nsx = "ns-1", "ns-2", "ns-x" + true = true + ) + + uu := map[string]struct { + flags *genericclioptions.ConfigFlags + k9sFlags *config.Flags + err string + context string + cluster string + namespace string + }{ + "no-override": { + namespace: "default", + }, + "override-cluster": { + flags: &genericclioptions.ConfigFlags{ + KubeConfig: &cfgFile, + ClusterName: &cl1, + }, + cluster: "cl-1", + context: "ct-1-1", + namespace: client.DefaultNamespace, + }, + "override-cluster-context": { + flags: &genericclioptions.ConfigFlags{ + KubeConfig: &cfgFile, + ClusterName: &cl1, + Context: &ct2, + }, + cluster: "cl-1", + context: "ct-1-2", + namespace: "ns-2", + }, + "override-bad-cluster": { + flags: &genericclioptions.ConfigFlags{ + KubeConfig: &cfgFile, + ClusterName: &ns1, + }, + cluster: "cl-1", + context: "ct-1-1", + namespace: client.DefaultNamespace, + }, + "override-ns": { + flags: &genericclioptions.ConfigFlags{ + KubeConfig: &cfgFile, + Namespace: &ns2, + }, + cluster: "cl-1", + context: "ct-1-1", + namespace: "ns-2", + }, + "all-ns": { + flags: &genericclioptions.ConfigFlags{ + KubeConfig: &cfgFile, + Namespace: &ns2, + }, + k9sFlags: &config.Flags{ + AllNamespaces: &true, + }, + cluster: "cl-1", + context: "ct-1-1", + namespace: client.NamespaceAll, + }, + + "override-bad-ns": { + flags: &genericclioptions.ConfigFlags{ + KubeConfig: &cfgFile, + Namespace: &nsx, + }, + cluster: "cl-1", + context: "ct-1-1", + namespace: "ns-x", + }, + "override-context": { + flags: &genericclioptions.ConfigFlags{ + KubeConfig: &cfgFile, + Context: &ct2, + }, + cluster: "cl-1", + context: "ct-1-2", + namespace: "ns-2", + }, + "override-bad-context": { + flags: &genericclioptions.ConfigFlags{ + KubeConfig: &cfgFile, + Context: &ns1, + }, + err: `k8sflags. unable to activate context "ns-1": no context found for: "ns-1"`, + }, + "use-current-context": { + flags: &genericclioptions.ConfigFlags{ + KubeConfig: &cfgFile, + }, + cluster: "cl-1", + context: "ct-1-1", + namespace: client.DefaultNamespace, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + cfg := mock.NewMockConfig() - assert.Nil(t, cfg.Load("testdata/k9s.yml")) - cfg.SetActiveView("po") - assert.Equal(t, "po", cfg.ActiveView()) + err := cfg.Refine(u.flags, u.k9sFlags, client.NewConfig(u.flags)) + if err != nil { + assert.Equal(t, u.err, err.Error()) + } else { + assert.Nil(t, err) + assert.Equal(t, u.context, cfg.K9s.ActiveContextName()) + assert.Equal(t, u.namespace, cfg.ActiveNamespace()) + } + }) + } } -func TestConfigFavNamespaces(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) +func TestConfigValidate(t *testing.T) { + cfg := mock.NewMockConfig() + cfg.SetConnection(mock.NewMockConnection()) - assert.Nil(t, cfg.Load("testdata/k9s.yml")) - expectedNS := []string{"default", "kube-public", "istio-system", "all", "kube-system"} - assert.Equal(t, expectedNS, cfg.FavNamespaces()) + assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true)) + cfg.Validate() } -func TestConfigLoadOldCfg(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("testdata/k9s_old.yml")) +func TestConfigLoad(t *testing.T) { + cfg := mock.NewMockConfig() + + assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true)) + assert.Equal(t, 2, cfg.K9s.RefreshRate) + assert.Equal(t, int64(200), cfg.K9s.Logger.TailCount) + assert.Equal(t, 2000, cfg.K9s.Logger.BufferSize) } func TestConfigLoadCrap(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - assert.NotNil(t, cfg.Load("testdata/k9s_not_there.yml")) + cfg := mock.NewMockConfig() + + assert.NotNil(t, cfg.Load("testdata/configs/k9s_not_there.yaml", true)) } func TestConfigSaveFile(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - - mk := NewMockKubeSettings() - m.When(mk.CurrentContextName()).ThenReturn("minikube", nil) - m.When(mk.CurrentClusterName()).ThenReturn("minikube", nil) - m.When(mk.CurrentNamespaceName()).ThenReturn("default", nil) - m.When(mk.ClusterNames()).ThenReturn(map[string]struct{}{"minikube": {}, "fred": {}, "blee": {}}, nil) - m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"default"}) - - cfg := config.NewConfig(mk) - cfg.SetConnection(mc) - assert.Nil(t, cfg.Load("testdata/k9s.yml")) + cfg := mock.NewMockConfig() + + assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true)) + cfg.K9s.RefreshRate = 100 cfg.K9s.ReadOnly = true cfg.K9s.Logger.TailCount = 500 cfg.K9s.Logger.BufferSize = 800 - cfg.K9s.CurrentContext = "blee" - cfg.K9s.CurrentCluster = "blee" cfg.Validate() - path := filepath.Join("/tmp", "k9s.yml") - err := cfg.SaveFile(path) - assert.Nil(t, err) + path := filepath.Join("/tmp", "k9s.yaml") + assert.NoError(t, cfg.SaveFile(path)) raw, err := os.ReadFile(path) assert.Nil(t, err) - assert.Equal(t, expectedConfig, string(raw)) + ee, err := os.ReadFile("testdata/configs/expected.yaml") + assert.Nil(t, err) + assert.Equal(t, string(ee), string(raw)) } func TestConfigReset(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - - mk := NewMockKubeSettings() - m.When(mk.CurrentContextName()).ThenReturn("blee", nil) - m.When(mk.CurrentClusterName()).ThenReturn("blee", nil) - m.When(mk.CurrentNamespaceName()).ThenReturn("default", nil) - m.When(mk.ClusterNames()).ThenReturn(map[string]struct{}{"blee": {}}, nil) - m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"default"}) - - cfg := config.NewConfig(mk) - cfg.SetConnection(mc) - assert.Nil(t, cfg.Load("testdata/k9s.yml")) + cfg := mock.NewMockConfig() + assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true)) cfg.Reset() cfg.Validate() - path := filepath.Join("/tmp", "k9s.yml") - err := cfg.SaveFile(path) - assert.Nil(t, err) + path := filepath.Join("/tmp", "k9s.yaml") + assert.NoError(t, cfg.SaveFile(path)) - raw, err := os.ReadFile(path) + bb, err := os.ReadFile(path) + assert.Nil(t, err) + ee, err := os.ReadFile("testdata/configs/k9s.yaml") assert.Nil(t, err) - assert.Equal(t, resetConfig, string(raw)) + assert.Equal(t, string(ee), string(bb)) } // Helpers... @@ -254,182 +591,3 @@ func TestSetup(t *testing.T) { fmt.Println("Boom!", m, i) }) } - -type mockSettings struct { - flags *genericclioptions.ConfigFlags -} - -var _ config.KubeSettings = (*mockSettings)(nil) - -func newMockSettings(flags *genericclioptions.ConfigFlags) *mockSettings { - return &mockSettings{flags: flags} -} -func (m *mockSettings) CurrentContextName() (string, error) { - return *m.flags.Context, nil -} -func (m *mockSettings) CurrentClusterName() (string, error) { return "", nil } -func (m *mockSettings) CurrentNamespaceName() (string, error) { - return *m.flags.Namespace, nil -} -func (m *mockSettings) ClusterNames() (map[string]struct{}, error) { return nil, nil } - -// ---------------------------------------------------------------------------- -// Test Data... - -var expectedConfig = `k9s: - liveViewAutoRefresh: true - refreshRate: 100 - maxConnRetry: 5 - enableMouse: false - headless: false - logoless: false - crumbsless: false - readOnly: true - noExitOnCtrlC: false - noIcons: false - skipLatestRevCheck: false - logger: - tail: 500 - buffer: 800 - sinceSeconds: 300 - fullScreenLogs: false - textWrap: false - showTime: false - showJSON: false - currentContext: blee - currentCluster: blee - keepMissingClusters: false - clusters: - blee: - namespace: - active: default - lockFavorites: false - favorites: - - default - view: - active: po - featureGates: - nodeShell: false - shellPod: - image: busybox:1.35.0 - command: [] - args: [] - namespace: default - limits: - cpu: 100m - memory: 100Mi - labels: {} - portForwardAddress: localhost - fred: - namespace: - active: default - lockFavorites: false - favorites: - - default - - kube-public - - istio-system - - all - - kube-system - view: - active: po - featureGates: - nodeShell: false - shellPod: - image: busybox:1.35.0 - command: [] - args: [] - namespace: default - limits: - cpu: 100m - memory: 100Mi - labels: {} - portForwardAddress: localhost - minikube: - namespace: - active: kube-system - lockFavorites: false - favorites: - - default - - kube-public - - istio-system - - all - - kube-system - view: - active: ctx - featureGates: - nodeShell: false - shellPod: - image: busybox:1.35.0 - command: [] - args: [] - namespace: default - limits: - cpu: 100m - memory: 100Mi - labels: {} - portForwardAddress: localhost - thresholds: - cpu: - critical: 90 - warn: 70 - memory: - critical: 90 - warn: 70 - screenDumpDir: /tmp - disablePodCounting: false -` - -var resetConfig = `k9s: - liveViewAutoRefresh: true - refreshRate: 2 - maxConnRetry: 5 - enableMouse: false - headless: false - logoless: false - crumbsless: false - readOnly: false - noExitOnCtrlC: false - noIcons: false - skipLatestRevCheck: false - logger: - tail: 200 - buffer: 2000 - sinceSeconds: 300 - fullScreenLogs: false - textWrap: false - showTime: false - showJSON: false - currentContext: blee - currentCluster: blee - keepMissingClusters: false - clusters: - blee: - namespace: - active: default - lockFavorites: false - favorites: - - default - view: - active: po - featureGates: - nodeShell: false - shellPod: - image: busybox:1.35.0 - command: [] - args: [] - namespace: default - limits: - cpu: 100m - memory: 100Mi - labels: {} - portForwardAddress: localhost - thresholds: - cpu: - critical: 90 - warn: 70 - memory: - critical: 90 - warn: 70 - screenDumpDir: /tmp - disablePodCounting: false -` diff --git a/internal/config/data/config.go b/internal/config/data/config.go new file mode 100644 index 0000000000..a03020f809 --- /dev/null +++ b/internal/config/data/config.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data + +import ( + "fmt" + "io" + "sync" + + "github.com/derailed/k9s/internal/client" + "gopkg.in/yaml.v2" + "k8s.io/client-go/tools/clientcmd/api" +) + +// Config tracks a context configuration. +type Config struct { + Context *Context `yaml:"k9s"` + mx sync.RWMutex +} + +// NewConfig returns a new config. +func NewConfig(ct *api.Context) *Config { + return &Config{ + Context: NewContextFromConfig(ct), + } +} + +func (c *Config) Merge(c1 *Config) { + if c1 == nil { + return + } + if c.Context != nil && c1.Context != nil { + c.Context.merge(c1.Context) + } +} + +// Validate ensures config is in norms. +func (c *Config) Validate(conn client.Connection, ks KubeSettings) { + c.mx.Lock() + defer c.mx.Unlock() + + if c.Context == nil { + c.Context = NewContext() + } + c.Context.Validate(conn, ks) +} + +// Dump used for debugging. +func (c *Config) Dump(w io.Writer) { + bb, _ := yaml.Marshal(&c) + + fmt.Fprintf(w, "%s\n", string(bb)) +} diff --git a/internal/config/data/context.go b/internal/config/data/context.go new file mode 100644 index 0000000000..8feb0a9bd0 --- /dev/null +++ b/internal/config/data/context.go @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data + +import ( + "os" + "sync" + + "github.com/derailed/k9s/internal/client" + "k8s.io/client-go/tools/clientcmd/api" +) + +// Context tracks K9s context configuration. +type Context struct { + ClusterName string `yaml:"cluster,omitempty"` + ReadOnly *bool `yaml:"readOnly,omitempty"` + Skin string `yaml:"skin,omitempty"` + Namespace *Namespace `yaml:"namespace"` + View *View `yaml:"view"` + FeatureGates FeatureGates `yaml:"featureGates"` + PortForwardAddress string `yaml:"portForwardAddress"` + mx sync.RWMutex +} + +// NewContext creates a new cluster configuration. +func NewContext() *Context { + return &Context{ + Namespace: NewNamespace(), + View: NewView(), + PortForwardAddress: defaultPFAddress(), + FeatureGates: NewFeatureGates(), + } +} + +// NewContextFromConfig returns a config based on a kubecontext. +func NewContextFromConfig(cfg *api.Context) *Context { + ct := NewContext() + ct.Namespace, ct.ClusterName = NewActiveNamespace(cfg.Namespace), cfg.Cluster + + return ct + +} + +// NewContextFromKubeConfig returns a new instance based on kubesettings or an error. +func NewContextFromKubeConfig(ks KubeSettings) (*Context, error) { + ct, err := ks.CurrentContext() + if err != nil { + return nil, err + } + + return NewContextFromConfig(ct), nil +} + +func (c *Context) merge(old *Context) { + if old == nil || old.Namespace == nil { + return + } + if c.Namespace == nil { + c.Namespace = NewNamespace() + } + c.Namespace.merge(old.Namespace) +} + +func (c *Context) GetClusterName() string { + c.mx.RLock() + defer c.mx.RUnlock() + + return c.ClusterName +} + +// Validate ensures a context config is tip top. +func (c *Context) Validate(conn client.Connection, ks KubeSettings) { + c.mx.Lock() + defer c.mx.Unlock() + + if a := os.Getenv(envPFAddress); a != "" { + c.PortForwardAddress = a + } + if c.PortForwardAddress == "" { + c.PortForwardAddress = defaultPFAddress() + } + if cl, err := ks.CurrentClusterName(); err == nil { + c.ClusterName = cl + } + if b := os.Getenv(envFGNodeShell); b != "" { + c.FeatureGates.NodeShell = defaultFGNodeShell() + } + + if c.Namespace == nil { + c.Namespace = NewNamespace() + } + c.Namespace.Validate(conn) + + if c.View == nil { + c.View = NewView() + } + c.View.Validate() +} diff --git a/internal/config/data/context_int_test.go b/internal/config/data/context_int_test.go new file mode 100644 index 0000000000..b4a78b2421 --- /dev/null +++ b/internal/config/data/context_int_test.go @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_contextMerge(t *testing.T) { + uu := map[string]struct { + c1, c2, e *Context + }{ + "empty": {}, + "nil": { + c1: &Context{ + Namespace: &Namespace{ + Active: "ns1", + Favorites: []string{"ns1", "ns2", "ns3"}, + }, + }, + e: &Context{ + Namespace: &Namespace{ + Active: "ns1", + Favorites: []string{"ns1", "ns2", "ns3"}, + }, + }, + }, + "deltas": { + c1: &Context{ + Namespace: &Namespace{ + Active: "ns1", + Favorites: []string{"ns1", "ns2", "ns3"}, + }, + }, + c2: &Context{ + Namespace: &Namespace{ + Active: "ns10", + Favorites: []string{"ns10", "ns11", "ns12"}, + }, + }, + e: &Context{ + Namespace: &Namespace{ + Active: "ns1", + Favorites: []string{"ns1", "ns2", "ns3", "ns10", "ns11", "ns12"}, + }, + }, + }, + "deltas-locked": { + c1: &Context{ + Namespace: &Namespace{ + Active: "ns1", + LockFavorites: true, + Favorites: []string{"ns1", "ns2", "ns3"}, + }, + }, + c2: &Context{ + Namespace: &Namespace{ + Active: "ns10", + Favorites: []string{"ns10", "ns11", "ns12"}, + }, + }, + e: &Context{ + Namespace: &Namespace{ + Active: "ns1", + LockFavorites: true, + Favorites: []string{"ns1", "ns2", "ns3"}, + }, + }, + }, + "no-namespace": { + c1: NewContext(), + c2: &Context{}, + e: NewContext(), + }, + } + + for k, u := range uu { + t.Run(k, func(t *testing.T) { + u.c1.merge(u.c2) + assert.Equal(t, u.e, u.c1) + }) + } +} diff --git a/internal/config/data/context_test.go b/internal/config/data/context_test.go new file mode 100644 index 0000000000..d66c8d5689 --- /dev/null +++ b/internal/config/data/context_test.go @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/config/data" + "github.com/derailed/k9s/internal/config/mock" + "github.com/stretchr/testify/assert" +) + +func TestClusterValidate(t *testing.T) { + c := data.NewContext() + c.Validate(mock.NewMockConnection(), mock.NewMockKubeSettings(makeFlags("cl-1", "ct-1"))) + + assert.Equal(t, "po", c.View.Active) + assert.Equal(t, "default", c.Namespace.Active) + assert.Equal(t, 1, len(c.Namespace.Favorites)) + assert.Equal(t, []string{"default"}, c.Namespace.Favorites) +} + +func TestClusterValidateEmpty(t *testing.T) { + c := data.NewContext() + c.Validate(mock.NewMockConnection(), mock.NewMockKubeSettings(makeFlags("cl-1", "ct-1"))) + + assert.Equal(t, "po", c.View.Active) + assert.Equal(t, "default", c.Namespace.Active) + assert.Equal(t, 1, len(c.Namespace.Favorites)) + assert.Equal(t, []string{"default"}, c.Namespace.Favorites) +} diff --git a/internal/config/data/dir.go b/internal/config/data/dir.go new file mode 100644 index 0000000000..3b045578f9 --- /dev/null +++ b/internal/config/data/dir.go @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data + +import ( + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "sync" + + "github.com/derailed/k9s/internal/config/json" + "github.com/rs/zerolog/log" + "gopkg.in/yaml.v2" + "k8s.io/client-go/tools/clientcmd/api" +) + +// Dir tracks context configurations. +type Dir struct { + root string + mx sync.Mutex +} + +// NewDir returns a new instance. +func NewDir(root string) *Dir { + return &Dir{ + root: root, + } +} + +// Load loads context configuration. +func (d *Dir) Load(n string, ct *api.Context) (*Config, error) { + if ct == nil { + return nil, errors.New("api.Context must not be nil") + } + var path = filepath.Join(d.root, SanitizeContextSubpath(ct.Cluster, n), MainConfigFile) + + f, err := os.Stat(path) + if errors.Is(err, fs.ErrPermission) { + return nil, err + } + if errors.Is(err, fs.ErrNotExist) || (f != nil && f.Size() == 0) { + log.Debug().Msgf("Context config not found! Generating... %q", path) + return d.genConfig(path, ct) + } + if err != nil { + return nil, err + } + + return d.loadConfig(path) +} + +func (d *Dir) genConfig(path string, ct *api.Context) (*Config, error) { + cfg := NewConfig(ct) + if err := d.Save(path, cfg); err != nil { + return nil, err + } + + return cfg, nil +} + +func (d *Dir) Save(path string, c *Config) error { + if cfg, err := d.loadConfig(path); err == nil { + c.Merge(cfg) + } + + d.mx.Lock() + defer d.mx.Unlock() + + if err := EnsureDirPath(path, DefaultDirMod); err != nil { + return err + } + cfg, err := yaml.Marshal(c) + if err != nil { + return err + } + + return os.WriteFile(path, cfg, DefaultFileMod) +} + +func (d *Dir) loadConfig(path string) (*Config, error) { + d.mx.Lock() + defer d.mx.Unlock() + + bb, err := os.ReadFile(path) + if err != nil { + return nil, err + } + if err := JSONValidator.Validate(json.ContextSchema, bb); err != nil { + return nil, fmt.Errorf("validation failed for %q: %w", path, err) + } + + var cfg Config + if err := yaml.Unmarshal(bb, &cfg); err != nil { + return nil, fmt.Errorf("context-config yaml load failed: %w\n%s", err, string(bb)) + } + + return &cfg, nil +} diff --git a/internal/config/data/dir_test.go b/internal/config/data/dir_test.go new file mode 100644 index 0000000000..b780a585c6 --- /dev/null +++ b/internal/config/data/dir_test.go @@ -0,0 +1,110 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data_test + +import ( + "os" + "strings" + "testing" + + "github.com/derailed/k9s/internal/config/data" + "github.com/derailed/k9s/internal/config/mock" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v2" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +func init() { + zerolog.SetGlobalLevel(zerolog.FatalLevel) +} + +func TestDirLoad(t *testing.T) { + uu := map[string]struct { + dir string + flags *genericclioptions.ConfigFlags + err error + cfg *data.Config + }{ + "happy-cl-1-ct-1": { + dir: "testdata/data/k9s", + flags: makeFlags("cl-1", "ct-1-1"), + cfg: mustLoadConfig("testdata/configs/ct-1-1.yaml"), + }, + + "happy-cl-1-ct2": { + dir: "testdata/data/k9s", + flags: makeFlags("cl-1", "ct-1-2"), + cfg: mustLoadConfig("testdata/configs/ct-1-2.yaml"), + }, + + "happy-cl-2": { + dir: "testdata/data/k9s", + flags: makeFlags("cl-2", "ct-2-1"), + cfg: mustLoadConfig("testdata/configs/ct-2-1.yaml"), + }, + + "toast": { + dir: "/tmp/data/k9s", + flags: makeFlags("cl-test", "ct-test-1"), + cfg: mustLoadConfig("testdata/configs/def_ct.yaml"), + }, + + "non-sanitized-path": { + dir: "/tmp/data/k9s", + flags: makeFlags("arn:aws:eks:eu-central-1:xxx:cluster/fred-blee", "fred-blee"), + cfg: mustLoadConfig("testdata/configs/aws_ct.yaml"), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.NotNil(t, u.cfg, "test config must not be nil") + if u.cfg == nil { + return + } + + ks := mock.NewMockKubeSettings(u.flags) + if strings.Index(u.dir, "/tmp") == 0 { + assert.NoError(t, mock.EnsureDir(u.dir)) + } + + d := data.NewDir(u.dir) + ct, err := ks.CurrentContext() + assert.NoError(t, err) + if err != nil { + return + } + + cfg, err := d.Load(*u.flags.Context, ct) + assert.Equal(t, u.err, err) + if u.err == nil { + assert.Equal(t, u.cfg, cfg) + } + }) + } +} + +// Helpers... + +func makeFlags(cl, ct string) *genericclioptions.ConfigFlags { + return &genericclioptions.ConfigFlags{ + ClusterName: &cl, + Context: &ct, + } +} + +func mustLoadConfig(cfg string) *data.Config { + bb, err := os.ReadFile(cfg) + if err != nil { + return nil + } + var ct data.Config + if err = yaml.Unmarshal(bb, &ct); err != nil { + return nil + } + + return &ct +} diff --git a/internal/config/data/feature_gate.go b/internal/config/data/feature_gate.go new file mode 100644 index 0000000000..8631c309a8 --- /dev/null +++ b/internal/config/data/feature_gate.go @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data + +// FeatureGates represents K9s opt-in features. +type FeatureGates struct { + NodeShell bool `yaml:"nodeShell"` +} + +// NewFeatureGates returns a new feature gate. +func NewFeatureGates() FeatureGates { + return FeatureGates{} +} diff --git a/internal/config/data/helpers.go b/internal/config/data/helpers.go new file mode 100644 index 0000000000..de2d7c7c1b --- /dev/null +++ b/internal/config/data/helpers.go @@ -0,0 +1,72 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data + +import ( + "errors" + "io/fs" + "os" + "path/filepath" + "regexp" +) + +const ( + envPFAddress = "K9S_DEFAULT_PF_ADDRESS" + envFGNodeShell = "K9S_FEATURE_GATE_NODE_SHELL" + defaultPortFwdAddress = "localhost" +) + +var invalidPathCharsRX = regexp.MustCompile(`[:/]+`) + +// SanitizeContextSubpath ensure cluster/context produces a valid path. +func SanitizeContextSubpath(cluster, context string) string { + return filepath.Join(SanitizeFileName(cluster), SanitizeFileName(context)) +} + +// SanitizeFileName ensure file spec is valid. +func SanitizeFileName(name string) string { + return invalidPathCharsRX.ReplaceAllString(name, "-") +} + +func defaultPFAddress() string { + if a := os.Getenv(envPFAddress); a != "" { + return a + } + + return defaultPortFwdAddress +} + +func defaultFGNodeShell() bool { + if a := os.Getenv(envFGNodeShell); a != "" { + return a == "true" + } + + return false +} + +// InList check if string is in a collection of strings. +func InList(ll []string, n string) bool { + for _, l := range ll { + if l == n { + return true + } + } + return false +} + +// EnsureDirPath ensures a directory exist from the given path. +func EnsureDirPath(path string, mod os.FileMode) error { + return EnsureFullPath(filepath.Dir(path), mod) +} + +// EnsureFullPath ensures a directory exist from the given path. +func EnsureFullPath(path string, mod os.FileMode) error { + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { + if err = os.MkdirAll(path, mod); err != nil { + return err + } + } + + return nil +} diff --git a/internal/config/data/helpers_test.go b/internal/config/data/helpers_test.go new file mode 100644 index 0000000000..0b41d4328e --- /dev/null +++ b/internal/config/data/helpers_test.go @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/derailed/k9s/internal/config/data" + "github.com/stretchr/testify/assert" +) + +func TestSanitizeFileName(t *testing.T) { + uu := map[string]struct { + file, e string + }{ + "empty": {}, + "plain": { + file: "bumble-bee-tuna", + e: "bumble-bee-tuna", + }, + "slash": { + file: "bumble/bee/tuna", + e: "bumble-bee-tuna", + }, + "column": { + file: "bumble::bee:tuna", + e: "bumble-bee-tuna", + }, + "eks": { + file: "arn:aws:eks:us-east-1:123456789:cluster/us-east-1-app-dev-common-eks", + e: "arn-aws-eks-us-east-1-123456789-cluster-us-east-1-app-dev-common-eks", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, data.SanitizeFileName(u.file)) + }) + } +} + +func TestHelperInList(t *testing.T) { + uu := []struct { + item string + list []string + expected bool + }{ + {"a", []string{}, false}, + {"", []string{}, false}, + {"", []string{""}, true}, + {"a", []string{"a", "b", "c", "d"}, true}, + {"z", []string{"a", "b", "c", "d"}, false}, + } + + for _, u := range uu { + assert.Equal(t, u.expected, data.InList(u.list, u.item)) + } +} + +func TestEnsureDirPathNone(t *testing.T) { + var mod os.FileMode = 0744 + dir := filepath.Join("/tmp", "fred") + os.Remove(dir) + + path := filepath.Join(dir, "duh.yaml") + assert.NoError(t, data.EnsureDirPath(path, mod)) + + p, err := os.Stat(dir) + assert.NoError(t, err) + assert.Equal(t, "drwxr--r--", p.Mode().String()) +} + +func TestEnsureDirPathNoOpt(t *testing.T) { + var mod os.FileMode = 0744 + dir := filepath.Join("/tmp", "k9s-test") + assert.NoError(t, os.RemoveAll(dir)) + assert.NoError(t, os.Mkdir(dir, mod)) + + path := filepath.Join(dir, "duh.yaml") + assert.NoError(t, data.EnsureDirPath(path, mod)) + + p, err := os.Stat(dir) + assert.NoError(t, err) + assert.Equal(t, "drwxr--r--", p.Mode().String()) +} diff --git a/internal/config/ns.go b/internal/config/data/ns.go similarity index 54% rename from internal/config/ns.go rename to internal/config/data/ns.go index 411f4fca96..819430356b 100644 --- a/internal/config/ns.go +++ b/internal/config/data/ns.go @@ -1,6 +1,11 @@ -package config +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data import ( + "sync" + "github.com/derailed/k9s/internal/client" "github.com/rs/zerolog/log" ) @@ -8,8 +13,6 @@ import ( const ( // MaxFavoritesNS number # favorite namespaces to keep in the configuration. MaxFavoritesNS = 9 - defaultNS = "default" - allNS = "all" ) // Namespace tracks active and favorites namespaces. @@ -17,33 +20,51 @@ type Namespace struct { Active string `yaml:"active"` LockFavorites bool `yaml:"lockFavorites"` Favorites []string `yaml:"favorites"` + mx sync.RWMutex } // NewNamespace create a new namespace configuration. func NewNamespace() *Namespace { + return NewActiveNamespace(client.DefaultNamespace) +} + +func NewActiveNamespace(n string) *Namespace { + if n == client.BlankNamespace { + n = client.DefaultNamespace + } + return &Namespace{ - Active: defaultNS, - Favorites: []string{defaultNS}, + Active: n, + Favorites: []string{client.DefaultNamespace}, } } -// Validate a namespace is setup correctly. -func (n *Namespace) Validate(c client.Connection, ks KubeSettings) { - if c == nil { - return - } - nns, err := c.ValidNamespaces() - if err != nil { +func (n *Namespace) merge(old *Namespace) { + n.mx.Lock() + defer n.mx.Unlock() + + if n.LockFavorites { return } - nn := client.NamespaceNames(nns) - if !n.isAllNamespaces() && !InList(nn, n.Active) { - log.Error().Msgf("[Config] Validation error active namespace %q does not exists", n.Active) + for _, fav := range old.Favorites { + if InList(n.Favorites, fav) { + continue + } + n.Favorites = append(n.Favorites, fav) } +} +// Validate validates a namespace is setup correctly. +func (n *Namespace) Validate(c client.Connection) { + n.mx.RLock() + defer n.mx.RUnlock() + + if c == nil || !c.IsValidNamespace(n.Active) { + return + } for _, ns := range n.Favorites { - if ns != allNS && !InList(nn, ns) { - log.Debug().Msgf("[Config] Invalid favorite found '%s' - %t", ns, n.isAllNamespaces()) + if !c.IsValidNamespace(ns) { + log.Debug().Msgf("[Namespace] Invalid favorite found '%s' - %t", ns, n.isAllNamespaces()) n.rmFavNS(ns) } } @@ -51,10 +72,18 @@ func (n *Namespace) Validate(c client.Connection, ks KubeSettings) { // SetActive set the active namespace. func (n *Namespace) SetActive(ns string, ks KubeSettings) error { - if ns == client.NotNamespaced { - ns = client.AllNamespaces + if n == nil { + n = NewActiveNamespace(ns) + } + + n.mx.Lock() + defer n.mx.Unlock() + + if ns == client.BlankNamespace { + ns = client.NamespaceAll } n.Active = ns + if ns != "" && !n.LockFavorites { n.addFavNS(ns) } @@ -63,7 +92,7 @@ func (n *Namespace) SetActive(ns string, ks KubeSettings) error { } func (n *Namespace) isAllNamespaces() bool { - return n.Active == allNS || n.Active == "" + return n.Active == client.NamespaceAll || n.Active == "" } func (n *Namespace) addFavNS(ns string) { @@ -82,6 +111,10 @@ func (n *Namespace) addFavNS(ns string) { } func (n *Namespace) rmFavNS(ns string) { + if n.LockFavorites { + return + } + victim := -1 for i, f := range n.Favorites { if f == ns { diff --git a/internal/config/data/ns_test.go b/internal/config/data/ns_test.go new file mode 100644 index 0000000000..d66b6903ed --- /dev/null +++ b/internal/config/data/ns_test.go @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/config/data" + "github.com/derailed/k9s/internal/config/mock" + "github.com/stretchr/testify/assert" +) + +func TestNSValidate(t *testing.T) { + ns := data.NewNamespace() + ns.Validate(mock.NewMockConnection()) + + assert.Equal(t, "default", ns.Active) + assert.Equal(t, []string{"default"}, ns.Favorites) +} + +func TestNSValidateMissing(t *testing.T) { + ns := data.NewNamespace() + ns.Validate(mock.NewMockConnection()) + + assert.Equal(t, "default", ns.Active) + assert.Equal(t, []string{"default"}, ns.Favorites) +} + +func TestNSValidateNoNS(t *testing.T) { + ns := data.NewNamespace() + ns.Validate(mock.NewMockConnection()) + + assert.Equal(t, "default", ns.Active) + assert.Equal(t, []string{"default"}, ns.Favorites) +} + +func TestNSSetActive(t *testing.T) { + allNS := []string{"ns4", "ns3", "ns2", "ns1", "all", "default"} + uu := []struct { + ns string + fav []string + }{ + {"all", []string{"all", "default"}}, + {"ns1", []string{"ns1", "all", "default"}}, + {"ns2", []string{"ns2", "ns1", "all", "default"}}, + {"ns3", []string{"ns3", "ns2", "ns1", "all", "default"}}, + {"ns4", allNS}, + } + + mk := mock.NewMockKubeSettings(makeFlags("cl-1", "ct-1")) + ns := data.NewNamespace() + for _, u := range uu { + err := ns.SetActive(u.ns, mk) + assert.Nil(t, err) + assert.Equal(t, u.ns, ns.Active) + assert.Equal(t, u.fav, ns.Favorites) + } +} + +func TestNSValidateRmFavs(t *testing.T) { + ns := data.NewNamespace() + ns.Favorites = []string{"default", "fred"} + ns.Validate(mock.NewMockConnection()) + + assert.Equal(t, []string{"default", "fred"}, ns.Favorites) +} diff --git a/internal/config/data/testdata/configs/aws_ct.yaml b/internal/config/data/testdata/configs/aws_ct.yaml new file mode 100644 index 0000000000..0f81b93b7f --- /dev/null +++ b/internal/config/data/testdata/configs/aws_ct.yaml @@ -0,0 +1,12 @@ +k9s: + cluster: arn:aws:eks:eu-central-1:xxx:cluster/fred-blee + namespace: + active: default + lockFavorites: false + favorites: + - default + view: + active: po + featureGates: + nodeShell: false + portForwardAddress: localhost diff --git a/internal/config/data/testdata/configs/ct-1-1.yaml b/internal/config/data/testdata/configs/ct-1-1.yaml new file mode 100644 index 0000000000..091b90719b --- /dev/null +++ b/internal/config/data/testdata/configs/ct-1-1.yaml @@ -0,0 +1,16 @@ +k9s: + cluster: cl-1 + skin: skin-1 + readOnly: false + namespace: + active: ns-1 + lockFavorites: true + favorites: + - default + - ns-1 + - ns-2 + view: + active: dp + featureGates: + nodeShell: true + portForwardAddress: localhost diff --git a/internal/config/data/testdata/configs/ct-1-2.yaml b/internal/config/data/testdata/configs/ct-1-2.yaml new file mode 100644 index 0000000000..e7bd28f509 --- /dev/null +++ b/internal/config/data/testdata/configs/ct-1-2.yaml @@ -0,0 +1,14 @@ +k9s: + cluster: cl-1 + skin: in_the_navy + readOnly: true + namespace: + active: default + lockFavorites: false + favorites: + - default + view: + active: po + featureGates: + nodeShell: false + portForwardAddress: localhost diff --git a/internal/config/data/testdata/configs/ct-2-1.yaml b/internal/config/data/testdata/configs/ct-2-1.yaml new file mode 100644 index 0000000000..5a2e25befe --- /dev/null +++ b/internal/config/data/testdata/configs/ct-2-1.yaml @@ -0,0 +1,15 @@ +k9s: + cluster: cl-2 + skin: skin-2 + readOnly: true + namespace: + active: ns-2 + lockFavorites: true + favorites: + - ns-1 + - ns-2 + view: + active: svc + featureGates: + nodeShell: true + portForwardAddress: fred diff --git a/internal/config/data/testdata/configs/def_ct.yaml b/internal/config/data/testdata/configs/def_ct.yaml new file mode 100644 index 0000000000..a69eb34685 --- /dev/null +++ b/internal/config/data/testdata/configs/def_ct.yaml @@ -0,0 +1,12 @@ +k9s: + cluster: cl-test + namespace: + active: default + lockFavorites: false + favorites: + - default + view: + active: po + featureGates: + nodeShell: false + portForwardAddress: localhost diff --git a/internal/config/data/testdata/data/k9s/cl-1/ct-1-1/config.yaml b/internal/config/data/testdata/data/k9s/cl-1/ct-1-1/config.yaml new file mode 100644 index 0000000000..091b90719b --- /dev/null +++ b/internal/config/data/testdata/data/k9s/cl-1/ct-1-1/config.yaml @@ -0,0 +1,16 @@ +k9s: + cluster: cl-1 + skin: skin-1 + readOnly: false + namespace: + active: ns-1 + lockFavorites: true + favorites: + - default + - ns-1 + - ns-2 + view: + active: dp + featureGates: + nodeShell: true + portForwardAddress: localhost diff --git a/internal/config/data/testdata/data/k9s/cl-1/ct-1-2/config.yaml b/internal/config/data/testdata/data/k9s/cl-1/ct-1-2/config.yaml new file mode 100644 index 0000000000..e7bd28f509 --- /dev/null +++ b/internal/config/data/testdata/data/k9s/cl-1/ct-1-2/config.yaml @@ -0,0 +1,14 @@ +k9s: + cluster: cl-1 + skin: in_the_navy + readOnly: true + namespace: + active: default + lockFavorites: false + favorites: + - default + view: + active: po + featureGates: + nodeShell: false + portForwardAddress: localhost diff --git a/internal/config/data/testdata/data/k9s/cl-2/ct-2-1/config.yaml b/internal/config/data/testdata/data/k9s/cl-2/ct-2-1/config.yaml new file mode 100644 index 0000000000..5a2e25befe --- /dev/null +++ b/internal/config/data/testdata/data/k9s/cl-2/ct-2-1/config.yaml @@ -0,0 +1,15 @@ +k9s: + cluster: cl-2 + skin: skin-2 + readOnly: true + namespace: + active: ns-2 + lockFavorites: true + favorites: + - ns-1 + - ns-2 + view: + active: svc + featureGates: + nodeShell: true + portForwardAddress: fred diff --git a/internal/config/data/types.go b/internal/config/data/types.go new file mode 100644 index 0000000000..4981be381e --- /dev/null +++ b/internal/config/data/types.go @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data + +import ( + "os" + + "github.com/derailed/k9s/internal/config/json" + "k8s.io/client-go/tools/clientcmd/api" +) + +// JSONValidator validate yaml configurations. +var JSONValidator = json.NewValidator() + +const ( + // DefaultDirMod default unix perms for k9s directory. + DefaultDirMod os.FileMode = 0744 + + // DefaultFileMod default unix perms for k9s files. + DefaultFileMod os.FileMode = 0600 + + // MainConfigFile track main configuration file.. + MainConfigFile = "config.yaml" +) + +// KubeSettings exposes kubeconfig context information. +type KubeSettings interface { + // CurrentContextName returns the name of the current context. + CurrentContextName() (string, error) + + // CurrentClusterName returns the name of the current cluster. + CurrentClusterName() (string, error) + + // CurrentNamespaceName returns the name of the current namespace. + CurrentNamespaceName() (string, error) + + // ContextNames returns all available context names. + ContextNames() (map[string]struct{}, error) + + // CurrentContext returns the current context configuration. + CurrentContext() (*api.Context, error) + + // GetContext returns a given context configuration or err if not found. + GetContext(string) (*api.Context, error) +} diff --git a/internal/config/view.go b/internal/config/data/view.go similarity index 61% rename from internal/config/view.go rename to internal/config/data/view.go index e9461c833f..044972ebe5 100644 --- a/internal/config/view.go +++ b/internal/config/data/view.go @@ -1,6 +1,9 @@ -package config +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s -const defaultView = "po" +package data + +const DefaultView = "po" // View tracks view configuration options. type View struct { @@ -9,12 +12,12 @@ type View struct { // NewView creates a new view configuration. func NewView() *View { - return &View{Active: defaultView} + return &View{Active: DefaultView} } // Validate a view configuration. func (v *View) Validate() { if len(v.Active) == 0 { - v.Active = defaultView + v.Active = DefaultView } } diff --git a/internal/config/view_test.go b/internal/config/data/view_test.go similarity index 64% rename from internal/config/view_test.go rename to internal/config/data/view_test.go index 2fc7532a90..100491fef6 100644 --- a/internal/config/view_test.go +++ b/internal/config/data/view_test.go @@ -1,14 +1,17 @@ -package config_test +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package data_test import ( "testing" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/data" "github.com/stretchr/testify/assert" ) func TestViewValidate(t *testing.T) { - v := config.NewView() + v := data.NewView() v.Validate() assert.Equal(t, "po", v.Active) @@ -19,7 +22,7 @@ func TestViewValidate(t *testing.T) { } func TestViewValidateBlank(t *testing.T) { - var v config.View + var v data.View v.Validate() assert.Equal(t, "po", v.Active) } diff --git a/internal/config/feature.go b/internal/config/feature.go index 52164ec5da..4b1fd75bcd 100644 --- a/internal/config/feature.go +++ b/internal/config/feature.go @@ -1,11 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config -// FeatureGates represents K9s opt-in features. -type FeatureGates struct { - NodeShell bool `yaml:"nodeShell"` -} +// // FeatureGates represents K9s opt-in features. +// type FeatureGates struct { +// NodeShell bool `yaml:"nodeShell"` +// } -// NewFeatureGates returns a new feature gate. -func NewFeatureGates() *FeatureGates { - return &FeatureGates{} -} +// // NewFeatureGates returns a new feature gate. +// func NewFeatureGates() *FeatureGates { +// return &FeatureGates{} +// } diff --git a/internal/config/files.go b/internal/config/files.go new file mode 100644 index 0000000000..2b246d5172 --- /dev/null +++ b/internal/config/files.go @@ -0,0 +1,283 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package config + +import ( + _ "embed" + "errors" + "io/fs" + "os" + "path/filepath" + + "github.com/derailed/k9s/internal/config/data" + + "github.com/adrg/xdg" + "github.com/rs/zerolog/log" +) + +const ( + // K9sEnvConfigDir represents k9s configuration dir env var. + K9sEnvConfigDir = "K9S_CONFIG_DIR" + + // K9sEnvLogsDir represents k9s logs dir env var. + K9sEnvLogsDir = "K9S_LOGS_DIR" + + // AppName tracks k9s app name. + AppName = "k9s" + + K9sLogsFile = "k9s.log" +) + +var ( + //go:embed templates/benchmarks.yaml + // benchmarkTpl tracks benchmark default config template + benchmarkTpl []byte + + //go:embed templates/aliases.yaml + // aliasesTpl tracks aliases default config template + aliasesTpl []byte + + //go:embed templates/hotkeys.yaml + // hotkeysTpl tracks hotkeys default config template + hotkeysTpl []byte + + //go:embed templates/stock-skin.yaml + // stockSkinTpl tracks stock skin template + stockSkinTpl []byte +) + +var ( + // AppConfigDir tracks main k9s config home directory. + AppConfigDir string + + // AppSkinsDir tracks skins data directory. + AppSkinsDir string + + // AppBenchmarksDir tracks benchmarks results directory. + AppBenchmarksDir string + + // AppDumpsDir tracks screen dumps data directory. + AppDumpsDir string + + // AppContextsDir tracks contexts data directory. + AppContextsDir string + + // AppConfigFile tracks k9s config file. + AppConfigFile string + + // AppLogFile tracks k9s logs file. + AppLogFile string + + // AppViewsFile tracks custom views config file. + AppViewsFile string + + // AppAliasesFile tracks aliases config file. + AppAliasesFile string + + // AppPluginsFile tracks plugins config file. + AppPluginsFile string + + // AppHotKeysFile tracks hotkeys config file. + AppHotKeysFile string +) + +// InitLogLoc initializes K9s logs location. +func InitLogLoc() error { + var appLogDir string + switch { + case isEnvSet(K9sEnvLogsDir): + appLogDir = os.Getenv(K9sEnvLogsDir) + case isEnvSet(K9sEnvConfigDir): + tmpDir, err := UserTmpDir() + if err != nil { + return err + } + appLogDir = tmpDir + default: + var err error + appLogDir, err = xdg.StateFile(AppName) + if err != nil { + return err + } + } + if err := data.EnsureFullPath(appLogDir, data.DefaultDirMod); err != nil { + return err + } + AppLogFile = filepath.Join(appLogDir, K9sLogsFile) + + return nil +} + +// InitLocs initializes k9s artifacts locations. +func InitLocs() error { + if isEnvSet(K9sEnvConfigDir) { + return initK9sEnvLocs() + } + + return initXDGLocs() +} + +func initK9sEnvLocs() error { + AppConfigDir = os.Getenv(K9sEnvConfigDir) + if err := data.EnsureFullPath(AppConfigDir, data.DefaultDirMod); err != nil { + return err + } + + AppDumpsDir = filepath.Join(AppConfigDir, "screen-dumps") + if err := data.EnsureFullPath(AppDumpsDir, data.DefaultDirMod); err != nil { + log.Warn().Err(err).Msgf("Unable to create screen-dumps dir: %s", AppDumpsDir) + } + AppBenchmarksDir = filepath.Join(AppConfigDir, "benchmarks") + if err := data.EnsureFullPath(AppBenchmarksDir, data.DefaultDirMod); err != nil { + log.Warn().Err(err).Msgf("Unable to create benchmarks dir: %s", AppBenchmarksDir) + } + AppSkinsDir = filepath.Join(AppConfigDir, "skins") + if err := data.EnsureFullPath(AppSkinsDir, data.DefaultDirMod); err != nil { + log.Warn().Err(err).Msgf("Unable to create skins dir: %s", AppSkinsDir) + } + AppContextsDir = filepath.Join(AppConfigDir, "clusters") + if err := data.EnsureFullPath(AppContextsDir, data.DefaultDirMod); err != nil { + log.Warn().Err(err).Msgf("Unable to create clusters dir: %s", AppContextsDir) + } + + AppConfigFile = filepath.Join(AppConfigDir, data.MainConfigFile) + AppHotKeysFile = filepath.Join(AppConfigDir, "hotkeys.yaml") + AppAliasesFile = filepath.Join(AppConfigDir, "aliases.yaml") + AppPluginsFile = filepath.Join(AppConfigDir, "plugins.yaml") + AppViewsFile = filepath.Join(AppConfigDir, "views.yaml") + + return nil +} + +func initXDGLocs() error { + var err error + + AppConfigDir, err = xdg.ConfigFile(AppName) + if err != nil { + return err + } + + AppConfigFile, err = xdg.ConfigFile(filepath.Join(AppName, data.MainConfigFile)) + if err != nil { + return err + } + + AppHotKeysFile = filepath.Join(AppConfigDir, "hotkeys.yaml") + AppAliasesFile = filepath.Join(AppConfigDir, "aliases.yaml") + AppPluginsFile = filepath.Join(AppConfigDir, "plugins.yaml") + AppViewsFile = filepath.Join(AppConfigDir, "views.yaml") + + AppSkinsDir = filepath.Join(AppConfigDir, "skins") + if err := data.EnsureFullPath(AppSkinsDir, data.DefaultDirMod); err != nil { + log.Warn().Err(err).Msgf("No skins dir detected") + } + + AppDumpsDir, err = xdg.StateFile(filepath.Join(AppName, "screen-dumps")) + if err != nil { + return err + } + + AppBenchmarksDir, err = xdg.StateFile(filepath.Join(AppName, "benchmarks")) + if err != nil { + log.Warn().Err(err).Msgf("No benchmarks dir detected") + } + + dataDir, err := xdg.DataFile(AppName) + if err != nil { + return err + } + AppContextsDir = filepath.Join(dataDir, "clusters") + if err := data.EnsureFullPath(AppContextsDir, data.DefaultDirMod); err != nil { + log.Warn().Err(err).Msgf("No context dir detected") + } + + return nil +} + +// AppContextDir generates a valid context config dir. +func AppContextDir(cluster, context string) string { + return filepath.Join(AppContextsDir, data.SanitizeContextSubpath(cluster, context)) +} + +// AppContextAliasesFile generates a valid context specific aliases file path. +func AppContextAliasesFile(cluster, context string) string { + return filepath.Join(AppContextsDir, data.SanitizeContextSubpath(cluster, context), "aliases.yaml") +} + +// AppContextPluginsFile generates a valid context specific plugins file path. +func AppContextPluginsFile(cluster, context string) string { + return filepath.Join(AppContextsDir, data.SanitizeContextSubpath(cluster, context), "plugins.yaml") +} + +// AppContextHotkeysFile generates a valid context specific hotkeys file path. +func AppContextHotkeysFile(cluster, context string) string { + return filepath.Join(AppContextsDir, data.SanitizeContextSubpath(cluster, context), "hotkeys.yaml") +} + +// AppContextConfig generates a valid context config file path. +func AppContextConfig(cluster, context string) string { + return filepath.Join(AppContextDir(cluster, context), data.MainConfigFile) +} + +// DumpsDir generates a valid context dump directory. +func DumpsDir(cluster, context string) (string, error) { + dir := filepath.Join(AppDumpsDir, data.SanitizeContextSubpath(cluster, context)) + + return dir, data.EnsureDirPath(dir, data.DefaultDirMod) +} + +// EnsureBenchmarksDir generates a valid benchmark results directory. +func EnsureBenchmarksDir(cluster, context string) (string, error) { + dir := filepath.Join(AppBenchmarksDir, data.SanitizeContextSubpath(cluster, context)) + + return dir, data.EnsureDirPath(dir, data.DefaultDirMod) +} + +// EnsureBenchmarksCfgFile generates a valid benchmark file. +func EnsureBenchmarksCfgFile(cluster, context string) (string, error) { + f := filepath.Join(AppContextDir(cluster, context), "benchmarks.yaml") + if err := data.EnsureDirPath(f, data.DefaultDirMod); err != nil { + return "", err + } + if _, err := os.Stat(f); errors.Is(err, fs.ErrNotExist) { + return f, os.WriteFile(f, benchmarkTpl, data.DefaultFileMod) + } + + return f, nil +} + +// EnsureAliasesCfgFile generates a valid aliases file. +func EnsureAliasesCfgFile() (string, error) { + f := filepath.Join(AppConfigDir, "aliases.yaml") + if err := data.EnsureDirPath(f, data.DefaultDirMod); err != nil { + return "", err + } + if _, err := os.Stat(f); errors.Is(err, fs.ErrNotExist) { + return f, os.WriteFile(f, aliasesTpl, data.DefaultFileMod) + } + + return f, nil +} + +// EnsureHotkeysCfgFile generates a valid hotkeys file. +func EnsureHotkeysCfgFile() (string, error) { + f := filepath.Join(AppConfigDir, "hotkeys.yaml") + if err := data.EnsureDirPath(f, data.DefaultDirMod); err != nil { + return "", err + } + if _, err := os.Stat(f); errors.Is(err, fs.ErrNotExist) { + return f, os.WriteFile(f, hotkeysTpl, data.DefaultFileMod) + } + + return f, nil +} + +// SkinFileFromName generate skin file path from spec. +func SkinFileFromName(n string) string { + if n == "" { + n = "stock" + } + + return filepath.Join(AppSkinsDir, n+".yaml") +} diff --git a/internal/config/files_int_test.go b/internal/config/files_int_test.go new file mode 100644 index 0000000000..e08766f1f1 --- /dev/null +++ b/internal/config/files_int_test.go @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package config + +import ( + "os" + "path/filepath" + "testing" + + "github.com/adrg/xdg" + "github.com/derailed/k9s/internal/config/data" + "github.com/stretchr/testify/assert" +) + +func Test_initXDGLocs(t *testing.T) { + tmp, err := UserTmpDir() + assert.NoError(t, err) + + os.Unsetenv("XDG_CONFIG_HOME") + os.Unsetenv("XDG_CACHE_HOME") + os.Unsetenv("XDG_STATE_HOME") + os.Unsetenv("XDG_DATA_HOME") + + os.Setenv("XDG_CONFIG_HOME", filepath.Join(tmp, "k9s-xdg", "config")) + os.Setenv("XDG_CACHE_HOME", filepath.Join(tmp, "k9s-xdg", "cache")) + os.Setenv("XDG_STATE_HOME", filepath.Join(tmp, "k9s-xdg", "state")) + os.Setenv("XDG_DATA_HOME", filepath.Join(tmp, "k9s-xdg", "data")) + xdg.Reload() + + uu := map[string]struct { + configDir string + configFile string + benchmarksDir string + contextsDir string + contextHotkeysFile string + contextConfig string + dumpsDir string + benchDir string + hkFile string + }{ + "check-env": { + configDir: filepath.Join(tmp, "k9s-xdg", "config", "k9s"), + configFile: filepath.Join(tmp, "k9s-xdg", "config", "k9s", data.MainConfigFile), + benchmarksDir: filepath.Join(tmp, "k9s-xdg", "state", "k9s", "benchmarks"), + contextsDir: filepath.Join(tmp, "k9s-xdg", "data", "k9s", "clusters"), + contextHotkeysFile: filepath.Join(tmp, "k9s-xdg", "data", "k9s", "clusters", "cl-1", "ct-1-1", "hotkeys.yaml"), + contextConfig: filepath.Join(tmp, "k9s-xdg", "data", "k9s", "clusters", "cl-1", "ct-1-1", data.MainConfigFile), + dumpsDir: filepath.Join(tmp, "k9s-xdg", "state", "k9s", "screen-dumps", "cl-1", "ct-1-1"), + benchDir: filepath.Join(tmp, "k9s-xdg", "state", "k9s", "benchmarks", "cl-1", "ct-1-1"), + hkFile: filepath.Join(tmp, "k9s-xdg", "config", "k9s", "hotkeys.yaml"), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.NoError(t, initXDGLocs()) + assert.Equal(t, u.configDir, AppConfigDir) + assert.Equal(t, u.configFile, AppConfigFile) + assert.Equal(t, u.benchmarksDir, AppBenchmarksDir) + assert.Equal(t, u.contextsDir, AppContextsDir) + assert.Equal(t, u.contextHotkeysFile, AppContextHotkeysFile("cl-1", "ct-1-1")) + assert.Equal(t, u.contextConfig, AppContextConfig("cl-1", "ct-1-1")) + dir, err := DumpsDir("cl-1", "ct-1-1") + assert.NoError(t, err) + assert.Equal(t, u.dumpsDir, dir) + bdir, err := EnsureBenchmarksDir("cl-1", "ct-1-1") + assert.NoError(t, err) + assert.Equal(t, u.benchDir, bdir) + hk, err := EnsureHotkeysCfgFile() + assert.NoError(t, err) + assert.Equal(t, u.hkFile, hk) + }) + } +} diff --git a/internal/config/files_test.go b/internal/config/files_test.go new file mode 100644 index 0000000000..02d53e4620 --- /dev/null +++ b/internal/config/files_test.go @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package config_test + +import ( + "os" + "path/filepath" + "testing" + + "github.com/adrg/xdg" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/data" + "github.com/stretchr/testify/assert" +) + +func TestInitLogLoc(t *testing.T) { + tmp, err := config.UserTmpDir() + assert.NoError(t, err) + + uu := map[string]struct { + dir string + e string + }{ + "log-env": { + dir: "/tmp/test/k9s/logs", + e: "/tmp/test/k9s/logs/k9s.log", + }, + "xdg-env": { + dir: "/tmp/test/xdg-state", + e: "/tmp/test/xdg-state/k9s/k9s.log", + }, + "cfg-env": { + dir: "/tmp/test/k9s-test", + e: filepath.Join(tmp, "k9s.log"), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + os.Unsetenv(config.K9sEnvLogsDir) + os.Unsetenv("XDG_STATE_HOME") + os.Unsetenv(config.K9sEnvConfigDir) + switch k { + case "log-env": + os.Setenv(config.K9sEnvLogsDir, u.dir) + case "xdg-env": + os.Setenv("XDG_STATE_HOME", u.dir) + xdg.Reload() + case "cfg-env": + os.Setenv(config.K9sEnvConfigDir, u.dir) + } + err := config.InitLogLoc() + assert.NoError(t, err) + assert.Equal(t, u.e, config.AppLogFile) + assert.NoError(t, os.RemoveAll(config.AppLogFile)) + }) + } +} + +func TestEnsureBenchmarkCfg(t *testing.T) { + os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config") + assert.NoError(t, config.InitLocs()) + defer assert.NoError(t, os.RemoveAll("/tmp/test-config")) + + assert.NoError(t, data.EnsureFullPath("/tmp/test-config/clusters/cl-1/ct-2", data.DefaultDirMod)) + assert.NoError(t, os.WriteFile("/tmp/test-config/clusters/cl-1/ct-2/benchmarks.yaml", []byte{}, data.DefaultFileMod)) + + uu := map[string]struct { + cluster, context string + f, e string + }{ + "not-exist": { + cluster: "cl-1", + context: "ct-1", + f: "/tmp/test-config/clusters/cl-1/ct-1/benchmarks.yaml", + e: "benchmarks:\n defaults:\n concurrency: 2\n requests: 200", + }, + "exist": { + cluster: "cl-1", + context: "ct-2", + f: "/tmp/test-config/clusters/cl-1/ct-2/benchmarks.yaml", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + f, err := config.EnsureBenchmarksCfgFile(u.cluster, u.context) + assert.NoError(t, err) + assert.Equal(t, u.f, f) + bb, err := os.ReadFile(f) + assert.NoError(t, err) + assert.Equal(t, u.e, string(bb)) + }) + } +} + +func TestSkinFileFromName(t *testing.T) { + config.AppSkinsDir = "/tmp/k9s-test/skins" + defer assert.NoError(t, os.RemoveAll("/tmp/k9s-test/skins")) + + uu := map[string]struct { + n string + e string + }{ + "empty": { + e: "/tmp/k9s-test/skins/stock.yaml", + }, + "happy": { + n: "fred-blee", + e: "/tmp/k9s-test/skins/fred-blee.yaml", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, config.SkinFileFromName(u.n)) + }) + } +} diff --git a/internal/config/flags.go b/internal/config/flags.go index e521ce2cf1..a1fc7699c3 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -1,10 +1,7 @@ -package config +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s -import ( - "fmt" - "os" - "path/filepath" -) +package config const ( // DefaultRefreshRate represents the refresh interval. @@ -17,9 +14,6 @@ const ( DefaultCommand = "" ) -// DefaultLogFile represents the default K9s log file. -var DefaultLogFile = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-%s.log", MustK9sUser())) - // Flags represents K9s configuration flags. type Flags struct { RefreshRate *int @@ -40,7 +34,7 @@ func NewFlags() *Flags { return &Flags{ RefreshRate: intPtr(DefaultRefreshRate), LogLevel: strPtr(DefaultLogLevel), - LogFile: strPtr(DefaultLogFile), + LogFile: strPtr(AppLogFile), Headless: boolPtr(false), Logoless: boolPtr(false), Command: strPtr(DefaultCommand), @@ -48,7 +42,7 @@ func NewFlags() *Flags { ReadOnly: boolPtr(false), Write: boolPtr(false), Crumbsless: boolPtr(false), - ScreenDumpDir: strPtr(K9sDefaultScreenDumpDir), + ScreenDumpDir: strPtr(AppDumpsDir), } } diff --git a/internal/config/flags_test.go b/internal/config/flags_test.go new file mode 100644 index 0000000000..907c47955a --- /dev/null +++ b/internal/config/flags_test.go @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package config_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/stretchr/testify/assert" +) + +func TestNewFlags(t *testing.T) { + config.AppDumpsDir = "/tmp/k9s-test/screen-dumps" + config.AppLogFile = "/tmp/k9s-test/k9s.log" + + f := config.NewFlags() + assert.Equal(t, 2, *f.RefreshRate) + assert.Equal(t, "info", *f.LogLevel) + assert.Equal(t, "/tmp/k9s-test/k9s.log", *f.LogFile) + assert.Equal(t, config.AppDumpsDir, *f.ScreenDumpDir) + assert.Empty(t, *f.Command) + assert.False(t, *f.Headless) + assert.False(t, *f.Logoless) + assert.False(t, *f.AllNamespaces) + assert.False(t, *f.ReadOnly) + assert.False(t, *f.Write) + assert.False(t, *f.Crumbsless) +} diff --git a/internal/config/helpers.go b/internal/config/helpers.go index 1ed01ab472..a62b3cd9c2 100644 --- a/internal/config/helpers.go +++ b/internal/config/helpers.go @@ -1,37 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config import ( "os" "os/user" "path/filepath" - "regexp" + "github.com/derailed/k9s/internal/config/data" "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" ) -const ( - // DefaultDirMod default unix perms for k9s directory. - DefaultDirMod os.FileMode = 0755 - // DefaultFileMod default unix perms for k9s files. - DefaultFileMod os.FileMode = 0600 -) +func isBoolSet(b *bool) bool { + return b != nil && *b +} + +func isStringSet(s *string) bool { + return s != nil && len(*s) > 0 +} -var invalidPathCharsRX = regexp.MustCompile(`[:]+`) +func isYamlFile(file string) bool { + ext := filepath.Ext(file) + return ext == ".yml" || ext == ".yaml" +} -// SanitizeFilename sanitizes the dump filename. -func SanitizeFilename(name string) string { - return invalidPathCharsRX.ReplaceAllString(name, "-") +// isEnvSet checks if env var is set. +func isEnvSet(env string) bool { + return os.Getenv(env) != "" } -// InList check if string is in a collection of strings. -func InList(ll []string, n string) bool { - for _, l := range ll { - if l == n { - return true - } +// UserTmpDir returns the temp dir with the current user name. +func UserTmpDir() (string, error) { + u, err := user.Current() + if err != nil { + return "", err } - return false + + dir := filepath.Join(os.TempDir(), u.Username, AppName) + + return dir, nil } // InNSList check if ns is in an ns collection. @@ -42,34 +51,26 @@ func InNSList(nn []interface{}, ns string) bool { ss[i] = nsp.Name } } - return InList(ss, ns) + return data.InList(ss, ns) } // MustK9sUser establishes current user identity or fail. func MustK9sUser() string { usr, err := user.Current() if err != nil { + envUsr := os.Getenv("USER") + if envUsr != "" { + return envUsr + } + envUsr = os.Getenv("LOGNAME") + if envUsr != "" { + return envUsr + } log.Fatal().Err(err).Msg("Die on retrieving user info") } return usr.Username } -// EnsureDirPath ensures a directory exist from the given path. -func EnsureDirPath(path string, mod os.FileMode) error { - return EnsureFullPath(filepath.Dir(path), mod) -} - -// EnsureFullPath ensures a directory exist from the given path. -func EnsureFullPath(path string, mod os.FileMode) error { - if _, err := os.Stat(path); os.IsNotExist(err) { - if err = os.MkdirAll(path, mod); err != nil { - return err - } - } - - return nil -} - // IsBoolSet checks if a bool prt is set. func IsBoolSet(b *bool) bool { return b != nil && *b diff --git a/internal/config/helpers_test.go b/internal/config/helpers_test.go index 381255853d..2c29e2d8d1 100644 --- a/internal/config/helpers_test.go +++ b/internal/config/helpers_test.go @@ -1,8 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config_test import ( - "os" - "path/filepath" "testing" "github.com/derailed/k9s/internal/config" @@ -11,24 +12,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func TestHelperInList(t *testing.T) { - uu := []struct { - item string - list []string - expected bool - }{ - {"a", []string{}, false}, - {"", []string{}, false}, - {"", []string{""}, true}, - {"a", []string{"a", "b", "c", "d"}, true}, - {"z", []string{"a", "b", "c", "d"}, false}, - } - - for _, u := range uu { - assert.Equal(t, u.expected, config.InList(u.list, u.item)) - } -} - func TestHelperInNSList(t *testing.T) { uu := []struct { item string @@ -55,30 +38,3 @@ func TestHelperInNSList(t *testing.T) { assert.Equal(t, u.expected, config.InNSList(u.list, u.item)) } } - -func TestEnsureDirPathNone(t *testing.T) { - var mod os.FileMode = 0744 - dir := filepath.Join("/tmp", "fred") - os.Remove(dir) - - path := filepath.Join(dir, "duh.yml") - assert.NoError(t, config.EnsureDirPath(path, mod)) - - p, err := os.Stat(dir) - assert.NoError(t, err) - assert.Equal(t, "drwxr--r--", p.Mode().String()) -} - -func TestEnsureDirPathNoOpt(t *testing.T) { - var mod os.FileMode = 0744 - dir := filepath.Join("/tmp", "blee") - os.Remove(dir) - assert.NoError(t, os.Mkdir(dir, mod)) - - path := filepath.Join(dir, "duh.yml") - assert.NoError(t, config.EnsureDirPath(path, mod)) - - p, err := os.Stat(dir) - assert.NoError(t, err) - assert.Equal(t, "drwxr--r--", p.Mode().String()) -} diff --git a/internal/config/hotkey.go b/internal/config/hotkey.go index 707bc0c718..65a651e40a 100644 --- a/internal/config/hotkey.go +++ b/internal/config/hotkey.go @@ -1,25 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config import ( + "errors" + "fmt" + "io/fs" "os" - "path/filepath" + "github.com/derailed/k9s/internal/config/data" + "github.com/derailed/k9s/internal/config/json" "gopkg.in/yaml.v2" ) -// K9sHotKeys manages K9s hotKeys. -var K9sHotKeys = filepath.Join(K9sHome(), "hotkey.yml") - // HotKeys represents a collection of plugins. type HotKeys struct { - HotKey map[string]HotKey `yaml:"hotKey"` + HotKey map[string]HotKey `yaml:"hotKeys"` } // HotKey describes a K9s hotkey. type HotKey struct { ShortCut string `yaml:"shortCut"` + Override bool `yaml:"override"` Description string `yaml:"description"` Command string `yaml:"command"` + KeepHistory bool `yaml:"keepHistory"` } // NewHotKeys returns a new plugin. @@ -30,19 +36,32 @@ func NewHotKeys() HotKeys { } // Load K9s plugins. -func (h HotKeys) Load() error { - return h.LoadHotKeys(K9sHotKeys) +func (h HotKeys) Load(path string) error { + if err := h.LoadHotKeys(AppHotKeysFile); err != nil { + return err + } + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { + return nil + } + + return h.LoadHotKeys(path) } // LoadHotKeys loads plugins from a given file. func (h HotKeys) LoadHotKeys(path string) error { - f, err := os.ReadFile(path) + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { + return nil + } + bb, err := os.ReadFile(path) if err != nil { return err } + if err := data.JSONValidator.Validate(json.HotkeysSchema, bb); err != nil { + return fmt.Errorf("validation failed for %q: %w", path, err) + } var hh HotKeys - if err := yaml.Unmarshal(f, &hh); err != nil { + if err := yaml.Unmarshal(bb, &hh); err != nil { return err } for k, v := range hh.HotKey { diff --git a/internal/config/hotkey_test.go b/internal/config/hotkey_test.go index 96515b243f..66c986afc5 100644 --- a/internal/config/hotkey_test.go +++ b/internal/config/hotkey_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config_test import ( @@ -9,7 +12,7 @@ import ( func TestHotKeyLoad(t *testing.T) { h := config.NewHotKeys() - assert.Nil(t, h.LoadHotKeys("testdata/hot_key.yml")) + assert.NoError(t, h.LoadHotKeys("testdata/hotkeys/hotkeys.yaml")) assert.Equal(t, 1, len(h.HotKey)) @@ -18,4 +21,5 @@ func TestHotKeyLoad(t *testing.T) { assert.Equal(t, "shift-0", k.ShortCut) assert.Equal(t, "Launch pod view", k.Description) assert.Equal(t, "pods", k.Command) + assert.Equal(t, true, k.KeepHistory) } diff --git a/internal/config/json/schemas/aliases.json b/internal/config/json/schemas/aliases.json new file mode 100644 index 0000000000..7ab1e6fcd8 --- /dev/null +++ b/internal/config/json/schemas/aliases.json @@ -0,0 +1,14 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "K9s aliases schema", + "type": "object", + "additionalProperties": false, + "properties": { + "aliases": { + "type": "object", + "additionalProperties": { "type": "string" }, + "required": [] + } + }, + "required": ["aliases"] +} diff --git a/internal/config/json/schemas/context.json b/internal/config/json/schemas/context.json new file mode 100644 index 0000000000..e392dd7e8d --- /dev/null +++ b/internal/config/json/schemas/context.json @@ -0,0 +1,44 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "K9s context config schema", + "type": "object", + "additionalProperties": false, + "properties": { + "k9s": { + "additionalProperties": false, + "properties": { + "cluster": { "type": "string" }, + "readOnly": {"type": "boolean"}, + "skin": { "type": "string" }, + "portForwardAddress": { "type": "string" }, + "namespace": { + "type": "object", + "additionalProperties": false, + "properties": { + "active": {"type": "string"}, + "lockFavorites": {"type": "boolean"}, + "favorites": { + "type": "array", + "items": {"type": "string"} + } + } + }, + "view": { + "type": "object", + "additionalProperties": false, + "properties": { + "active": { "type": "string" } + } + }, + "featureGates": { + "type": "object", + "additionalProperties": false, + "properties": { + "nodeShell": { "type": "boolean" } + } + } + } + } + }, + "required": ["k9s"] +} \ No newline at end of file diff --git a/internal/config/json/schemas/hotkeys.json b/internal/config/json/schemas/hotkeys.json new file mode 100644 index 0000000000..49906422d3 --- /dev/null +++ b/internal/config/json/schemas/hotkeys.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "K9s hotkeys schema", + "type": "object", + "additionalProperties": false, + "properties": { + "hotKeys": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "shortCut": {"type": "string"}, + "override": { "type": "boolean" }, + "description": {"type": "string"}, + "command": {"type": "string"}, + "keepHistory": {"type": "boolean"} + } + } + } + }, + "required": ["hotKeys"] +} diff --git a/internal/config/json/schemas/k9s.json b/internal/config/json/schemas/k9s.json new file mode 100644 index 0000000000..8f02c370c4 --- /dev/null +++ b/internal/config/json/schemas/k9s.json @@ -0,0 +1,134 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "K9s config schema", + "type": "object", + "additionalProperties": false, + "properties": { + "k9s": { + "additionalProperties": false, + "properties": { + "liveViewAutoRefresh": { "type": "boolean" }, + "screenDumpDir": {"type": "string"}, + "refreshRate": { "type": "integer" }, + "maxConnRetry": { "type": "integer" }, + "readOnly": { "type": "boolean" }, + "noExitOnCtrlC": { "type": "boolean" }, + "skipLatestRevCheck": { "type": "boolean" }, + "disablePodCounting": { "type": "boolean" }, + "ui": { + "type": "object", + "additionalProperties": false, + "properties": { + "enableMouse": {"type": "boolean"}, + "headless": {"type": "boolean"}, + "logoless": {"type": "boolean"}, + "crumbsless": {"type": "boolean"}, + "noIcons": {"type": "boolean"}, + "reactive": {"type": "boolean"}, + "skin": {"type": "string"}, + "defaultsToFullScreen": {"type": "boolean"} + } + }, + "shellPod": { + "type": "object", + "additionalProperties": true, + "properties": { + "image": { "type": "string" }, + "command": { + "type": "array", + "items": { "type": "string"} + }, + "args": { + "type": "array", + "items": { "type": "string"} + }, + "namespace": { "type": "string" }, + "limits": { + "type": "object", + "properties": { + "cpu": { "type": "string" }, + "memory": { "type": "string" } + }, + "required": ["cpu", "memory"] + }, + "labels": { + "type": "object", + "additionalProperties": { "type": "string" }, + "required": [] + }, + "tty": { "type": "boolean" }, + "imagePullPolicy": { "type": "string" }, + "imagePullSecrets": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string" } + } + } + } + }, + "required": ["image", "namespace", "limits"] + }, + "imageScans": { + "type": "object", + "additionalProperties": false, + "properties": { + "enable": { "type": "boolean" }, + "namespace": { "type": "string" }, + "exclusions": { + "type": "object", + "properties": { + "namespaces": { + "type": "array", + "items": { "type": "string" } + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { "type": "string" } + } + } + } + } + }, + "required": ["enable"] + }, + "logger": { + "type": "object", + "additionalProperties": false, + "properties": { + "tail": {"type": "integer"}, + "buffer": {"type": "integer"}, + "sinceSeconds": {"type": "integer"}, + "textWrap": {"type": "boolean"}, + "showTime": {"type": "boolean"}, + "showJSON": {"type": "boolean"} + } + }, + "thresholds": { + "type": "object", + "additionalProperties": false, + "properties": { + "cpu": { + "type": "object", + "properties": { + "critical": {"type": "integer"}, + "warn": {"type": "integer"} + } + }, + "memory": { + "type": "object", + "properties": { + "critical": {"type": "integer"}, + "warn": {"type": "integer"} + } + } + } + } + } + } + }, + "required": ["k9s"] +} diff --git a/internal/config/json/schemas/plugins.json b/internal/config/json/schemas/plugins.json new file mode 100644 index 0000000000..c1ba587a6c --- /dev/null +++ b/internal/config/json/schemas/plugins.json @@ -0,0 +1,36 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "K9s plugins schema", + "type": "object", + "additionalProperties": false, + "properties": { + "plugins": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "shortCut": { "type": "string" }, + "override": { "type": "boolean" }, + "description": { "type": "string" }, + "confirm": { "type": "boolean" }, + "dangerous": { "type": "boolean" }, + "scopes": { + "type": "array", + "items": { "type": "string" } + }, + "command": { "type": "string" }, + "background": { "type": "boolean" }, + "overwriteOutput": { "type": "boolean" }, + "args": { + "type": "array", + "items": { "type": ["string", "number"] } + } + }, + "required": ["shortCut", "description", "scopes", "command"] + }, + "required": [] + } + }, + "required": ["plugins"] +} diff --git a/internal/config/json/schemas/skin.json b/internal/config/json/schemas/skin.json new file mode 100644 index 0000000000..0dd6c07f22 --- /dev/null +++ b/internal/config/json/schemas/skin.json @@ -0,0 +1,185 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "K9s skin schema", + "type": "object", + "additionalProperties": true, + "properties": { + "k9s": { + "type": "object", + "additionalProperties": false, + "properties": { + "body": { + "type": "object", + "properties": { + "fgColor": {"type": "string"}, + "bgColor": {"type": "string"}, + "logoColor": {"type": "string"} + } + }, + "prompt": { + "type": "object", + "properties": { + "fgColor": {"type": "string"}, + "bgColor": {"type": "string"}, + "suggestColor": {"type": "string"} + } + }, + "info": { + "type": "object", + "properties": { + "fgColor": {"type": "string"}, + "sectionColor": {"type": "string"} + } + }, + "help": { + "type": "object", + "properties": { + "fgColor": {"type": "string"}, + "bgColor": {"type": "string"}, + "keyColor": {"type": "string"}, + "numKeyColor": {"type": "string"}, + "sectionColor": {"type": "string"} + } + }, + "dialog": { + "type": "object", + "properties": { + "fgColor": {"type": "string"}, + "bgColor": {"type": "string"}, + "buttonFgColor": {"type": "string"}, + "buttonBgColor": {"type": "string"}, + "buttonFocusFgColor": {"type": "string"}, + "buttonFocusBgColor": {"type": "string"}, + "labelFgColor": {"type": "string"}, + "fieldFgColor": {"type": "string"} + } + }, + "frame": { + "type": "object", + "properties": { + "border": { + "type": "object", + "properties": { + "fgColor": {"type": "string"}, + "bgColor": {"type": "string"} + } + }, + "menu": { + "type": "object", + "properties": { + "fgColor": {"type": "string"}, + "keyColor": {"type": "string"}, + "numKeyColor": {"type": "string"} + } + }, + "crumbs": { + "type": "object", + "properties": { + "fgColor": {"type": "string"}, + "keyColor": {"type": "string"}, + "activeColor": {"type": "string"} + } + }, + "status": { + "type": "object", + "properties": { + "newColor": {"type": "string"}, + "modifyColor": {"type": "string"}, + "addColor:": {"type": "string"}, + "errorColor": {"type": "string"}, + "highlightColor": {"type": "string"}, + "killColor": {"type": "string"}, + "completedColor": {"type": "string"} + } + }, + "title": { + "type": "object", + "properties": { + "fgColor": {"type": "string"}, + "bgColor":{"type": "string"}, + "highlightColor": {"type": "string"}, + "counterColor":{"type": "string"}, + "filterColor": {"type": "string"} + } + } + } + }, + "views": { + "type": "object", + "properties": { + "charts": { + "type": "object", + "properties": { + "bgColor": {"type": "string"}, + "defaultDialColors": { + "type": "array", + "items": {"type": "string"} + }, + "defaultChartColors": { + "type": "array", + "items": {"type": "string"} + } + }, + "table": { + "type": "object", + "properties": { + "fgColor": {"type": "string"}, + "bgColor": {"type": "string"}, + "cursorFgColor": {"type": "string"}, + "cursorBgColor": {"type": "string"}, + "header": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "fgColor": {"type": "string"}, + "bgColor": {"type": "string"} + } + } + } + } + }, + "xray": { + "type": "object", + "properties": { + "fgColor": {"type": "string"}, + "bgColor": {"type": "string"}, + "cursorFgColor": {"type": "string"}, + "graphicColor": {"type": "string"}, + "showIcons": {"type": "boolean"} + } + }, + "yaml": { + "type": "object", + "properties": { + "keyColor": {"type": "string"}, + "colonColor": {"type": "string"}, + "valueColor": {"type": "string"} + } + }, + "logs": { + "type": "object", + "properties": { + "fgColor": {"type": "string"}, + "bgColor": {"type": "string"}, + "indicator": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "fgColor": {"type": "string"}, + "bgColor": {"type": "string"}, + "toggleOnColor": {"type": "string"}, + "toggleOffColor": {"type": "string"} + } + } + } + } + } + } + } + } + } + } + } +} diff --git a/internal/config/json/schemas/views.json b/internal/config/json/schemas/views.json new file mode 100644 index 0000000000..6b3971c501 --- /dev/null +++ b/internal/config/json/schemas/views.json @@ -0,0 +1,24 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "K9s views schema", + "type": "object", + "additionalProperties": false, + "properties": { + "views": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "sortColumn": { "type": "string" }, + "columns": { + "type": "array", + "items": { "type": "string" } + } + }, + "required": ["columns"] + } + } + }, + "required": ["views"] +} diff --git a/internal/config/json/testdata/aliases/cool.yaml b/internal/config/json/testdata/aliases/cool.yaml new file mode 100644 index 0000000000..60f0867c70 --- /dev/null +++ b/internal/config/json/testdata/aliases/cool.yaml @@ -0,0 +1,3 @@ +aliases: + blee: duh + fred: zorg diff --git a/internal/config/json/testdata/aliases/toast.yaml b/internal/config/json/testdata/aliases/toast.yaml new file mode 100644 index 0000000000..3ba24ef386 --- /dev/null +++ b/internal/config/json/testdata/aliases/toast.yaml @@ -0,0 +1,3 @@ +alias: + blee: duh + fred: zorg diff --git a/internal/config/json/testdata/context/cool.yaml b/internal/config/json/testdata/context/cool.yaml new file mode 100644 index 0000000000..fd2c67718b --- /dev/null +++ b/internal/config/json/testdata/context/cool.yaml @@ -0,0 +1,15 @@ +k9s: + cluster: kind-dashb + readOnly: false + skin: nightfox + namespace: + active: default + lockFavorites: false + favorites: + - kube-system + - default + view: + active: pod + featureGates: + nodeShell: false + portForwardAddress: localhost diff --git a/internal/config/json/testdata/context/toast.yaml b/internal/config/json/testdata/context/toast.yaml new file mode 100644 index 0000000000..997914b4dc --- /dev/null +++ b/internal/config/json/testdata/context/toast.yaml @@ -0,0 +1,16 @@ +k9s: + cluster: kind-dashb + readOnly: false + skin: nightfox + namespaces: + active: default + lockFavorites: false + favorites: + - kube-system + - default + view: + active: pod + fred: blee + featureGates: + nodeShell: false + portForwardAddress: localhost diff --git a/internal/config/json/testdata/hotkeys/cool.yaml b/internal/config/json/testdata/hotkeys/cool.yaml new file mode 100644 index 0000000000..9e8e11d680 --- /dev/null +++ b/internal/config/json/testdata/hotkeys/cool.yaml @@ -0,0 +1,33 @@ +hotKey: + shift-0: + shortCut: Shift-0 + description: Popeye + command: popeye + shift-1: + shortCut: Shift-1 + description: View deployments + command: dp + shift-2: + shortCut: Shift-2 + description: View services + command: service + shift-3: + shortCut: Shift-3 + description: View statefulsets + command: sts + shift-4: + shortCut: Shift-4 + description: Xray Deployments + command: xray dp + shift-5: + shortCut: Shift-5 + description: Xray StatefulSets + command: xray sts + shift-6: + shortCut: Shift-6 + description: Xray DaemonSets + command: xray ds + shift-7: + shortCut: Shift-7 + description: Xray Services + command: xray svc diff --git a/internal/config/json/testdata/k9s/cool.yaml b/internal/config/json/testdata/k9s/cool.yaml new file mode 100644 index 0000000000..5261b9d7b0 --- /dev/null +++ b/internal/config/json/testdata/k9s/cool.yaml @@ -0,0 +1,39 @@ +k9s: + liveViewAutoRefresh: false + screenDumpDir: /Users/fernand/.local/state/k9s/screen-dumps + refreshRate: 2 + maxConnRetry: 5 + readOnly: false + noExitOnCtrlC: false + ui: + enableMouse: false + headless: false + logoless: false + crumbsless: false + noIcons: false + skipLatestRevCheck: false + disablePodCounting: false + shellPod: + image: busybox:1.35.0 + namespace: default + limits: + cpu: 100m + memory: 100Mi + imageScans: + enable: false + exclusions: + namespaces: [] + labels: {} + logger: + tail: 100 + buffer: 5000 + sinceSeconds: -1 + textWrap: false + showTime: false + thresholds: + cpu: + critical: 90 + warn: 70 + memory: + critical: 90 + warn: 70 diff --git a/internal/config/json/testdata/k9s/toast.yaml b/internal/config/json/testdata/k9s/toast.yaml new file mode 100644 index 0000000000..61dcda41db --- /dev/null +++ b/internal/config/json/testdata/k9s/toast.yaml @@ -0,0 +1,33 @@ +k9s: + liveViewAutoRefresh: false + screenDumpDir: /Users/fernand/.local/state/k9s/screen-dumps + refreshRate: 2 + maxConnRetry: 5 + readOnly: false + noExitOnCtrlC: false + skipLatestRevCheck: false + disablePodCounting: false + shellPods: + image: busybox:1.35.0 + namespace: default + limits: + cpu: 100m + memory: 100Mi + imageScans: + enable: false + exclusions: + namespaces: [] + labels: {} + logger: + tail: 100 + buffer: 5000 + sinceSeconds: -1 + textWrap: false + showTime: false + thresholds: + cpu: + critical: 90 + warn: 70 + memory: + critical: 90 + warn: 70 diff --git a/internal/config/json/testdata/plugins/cool.yaml b/internal/config/json/testdata/plugins/cool.yaml new file mode 100644 index 0000000000..bfc98dcd3b --- /dev/null +++ b/internal/config/json/testdata/plugins/cool.yaml @@ -0,0 +1,23 @@ +plugins: + blee: + shortCut: g + confirm: false + description: blee + scopes: + - namespaces + command: sh + background: false + args: + - -c + - "blee bla" + duh: + shortCut: h + confirm: true + description: duh + scopes: + - all + command: sh + background: true + args: + - -c + - "duh fred" diff --git a/internal/config/json/testdata/plugins/toast.yaml b/internal/config/json/testdata/plugins/toast.yaml new file mode 100644 index 0000000000..43adeb7a47 --- /dev/null +++ b/internal/config/json/testdata/plugins/toast.yaml @@ -0,0 +1,21 @@ +plugins: + blee: + shortCuts: g + confirm: false + description: blee + scopes: + - namespaces + command: sh + background: false + args: + - -c + - "blee bla" + duh: + shortCut: h + confirm: true + description: duh + command: sh + background: true + args: + - -c + - "duh fred" diff --git a/internal/config/json/testdata/skins/cool.yaml b/internal/config/json/testdata/skins/cool.yaml new file mode 100644 index 0000000000..187d344b70 --- /dev/null +++ b/internal/config/json/testdata/skins/cool.yaml @@ -0,0 +1,109 @@ +# ----------------------------------------------------------------------------- +# K9s Nightfox Theme +# Based on the Nightfox.nvim color scheme: +# https://github.com/EdenEast/nightfox.nvim +# ----------------------------------------------------------------------------- + +# Styles... +foreground: &foreground "#cdcecf" +background: &background "#192330" +current_line: ¤t_line "#2b3b51" +selection: &selection "#2b3b51" +comment: &comment "#738091" +cyan: &cyan "#63cdcf" +green: &green "#81b29a" +orange: &orange "#f4a261" +magenta: &magenta "#9d79d6" +blue: &blue "#719cd6" +red: &red "#c94f6d" + +# Skin... +k9s: + body: + fgColor: *foreground + bgColor: *background + logoColor: *blue + prompt: + fgColor: *foreground + bgColor: *background + suggestColor: *orange + info: + fgColor: *magenta + sectionColor: *foreground + help: + fgColor: *foreground + bgColor: *background + keyColor: *magenta + numKeyColor: *magenta + sectionColor: *foreground + dialog: + fgColor: *foreground + bgColor: *background + buttonFgColor: *foreground + buttonBgColor: *magenta + buttonFocusFgColor: white + buttonFocusBgColor: *cyan + labelFgColor: *orange + fieldFgColor: *foreground + frame: + border: + fgColor: *selection + focusColor: *current_line + menu: + fgColor: *foreground + keyColor: *magenta + numKeyColor: *magenta + crumbs: + fgColor: *foreground + bgColor: *current_line + activeColor: *current_line + status: + newColor: *cyan + modifyColor: *blue + addColor: *green + errorColor: *red + highlightColor: *orange + killColor: *comment + completedColor: *comment + title: + fgColor: *foreground + bgColor: *current_line + highlightColor: *orange + counterColor: *blue + filterColor: *magenta + views: + charts: + bgColor: default + defaultDialColors: + - *blue + - *red + defaultChartColors: + - *blue + - *red + table: + fgColor: *foreground + bgColor: *background + cursorFgColor: *selection + cursorBgColor: *current_line + header: + fgColor: *foreground + bgColor: *background + sorterColor: *cyan + xray: + fgColor: *foreground + bgColor: *background + cursorColor: *current_line + graphicColor: *blue + showIcons: false + yaml: + keyColor: *magenta + colonColor: *blue + valueColor: *foreground + logs: + fgColor: *foreground + bgColor: *background + indicator: + fgColor: *foreground + bgColor: *selection + toggleOnColor: *magenta + toggleOffColor: *blue diff --git a/internal/config/json/testdata/skins/toast.yaml b/internal/config/json/testdata/skins/toast.yaml new file mode 100644 index 0000000000..271bf6a5a0 --- /dev/null +++ b/internal/config/json/testdata/skins/toast.yaml @@ -0,0 +1,103 @@ +# ----------------------------------------------------------------------------- +# K9s Nightfox Theme +# Based on the Nightfox.nvim color scheme: +# https://github.com/EdenEast/nightfox.nvim +# ----------------------------------------------------------------------------- + +# Styles... +foreground: &foreground "#cdcecf" +background: &background "#192330" +current_line: ¤t_line "#2b3b51" +selection: &selection "#2b3b51" +comment: &comment "#738091" +cyan: &cyan "#63cdcf" +green: &green "#81b29a" +orange: &orange "#f4a261" +magenta: &magenta "#9d79d6" +blue: &blue "#719cd6" +red: &red "#c94f6d" + +# Skin... +k9s: + bodys: + fgColor: *foreground + bgColor: *background + logoColor: *blue + prompt: + fgColor: *foreground + bgColor: *background + suggestColor: *orange + info: + fgColor: *magenta + sectionColor: *foreground + dialog: + fgColor: *foreground + bgColor: *background + buttonFgColor: *foreground + buttonBgColor: *magenta + buttonFocusFgColor: white + buttonFocusBgColor: *cyan + labelFgColor: *orange + fieldFgColor: *foreground + frame: + border: + fgColor: *selection + focusColor: *current_line + menu: + fgColor: *foreground + keyColor: *magenta + numKeyColor: *magenta + crumbs: + fgColor: *foreground + bgColor: *current_line + activeColor: *current_line + status: + newColor: *cyan + modifyColor: *blue + addColor: *green + errorColor: *red + highlightColor: *orange + killColor: *comment + completedColor: *comment + title: + fgColor: *foreground + bgColor: *current_line + highlightColor: *orange + counterColor: *blue + filterColor: *magenta + views: + charts: + bgColor: default + defaultDialColors: + - *blue + - *red + defaultChartColors: + - *blue + - *red + table: + fgColor: *foreground + bgColor: *background + cursorFgColor: *selection + cursorBgColor: *current_line + header: + fgColor: *foreground + bgColor: *background + sorterColor: *cyan + xray: + fgColor: *foreground + bgColor: *background + cursorColor: *current_line + graphicColor: *blue + showIcons: false + yaml: + keyColor: *magenta + colonColor: *blue + valueColor: *foreground + logs: + fgColor: *foreground + bgColor: *background + indicator: + fgColor: *foreground + bgColor: *selection + toggleOnColor: *magenta + toggleOffColor: *blue diff --git a/internal/config/json/testdata/views/cool.yaml b/internal/config/json/testdata/views/cool.yaml new file mode 100644 index 0000000000..ce89ceb8ba --- /dev/null +++ b/internal/config/json/testdata/views/cool.yaml @@ -0,0 +1,12 @@ +views: + v1/nodes: + columns: + - NAME + - IP + v1/endpoints: + sortColumn: AGE:asc + columns: + - NAME + - NAMESPACE + - ENDPOINTS + - AGE diff --git a/internal/config/json/testdata/views/toast.yaml b/internal/config/json/testdata/views/toast.yaml new file mode 100644 index 0000000000..42ec1ae470 --- /dev/null +++ b/internal/config/json/testdata/views/toast.yaml @@ -0,0 +1,9 @@ +views: + v1/nodes: + v1/endpoints: + sortCol: AGE:asc + cols: + - NAME + - NAMESPACE + - ENDPOINTS + - AGE diff --git a/internal/config/json/validator.go b/internal/config/json/validator.go new file mode 100644 index 0000000000..e9bb02119c --- /dev/null +++ b/internal/config/json/validator.go @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package json + +import ( + "cmp" + _ "embed" + "errors" + "fmt" + "slices" + + "github.com/rs/zerolog/log" + "github.com/xeipuuv/gojsonschema" + "gopkg.in/yaml.v3" +) + +const ( + // PluginsSchema describes plugins schema. + PluginsSchema = "plugins.json" + + // AliasesSchema describes aliases schema. + AliasesSchema = "aliases.json" + + // ViewsSchema describes views schema. + ViewsSchema = "views.json" + + // HotkeysSchema describes hotkeys schema. + HotkeysSchema = "hotkeys.json" + + // K9sSchema describes k9s config schema. + K9sSchema = "k9s.json" + + // ContextSchema describes context config schema. + ContextSchema = "context.json" + + // SkinSchema describes skin config schema. + SkinSchema = "skin.json" +) + +var ( + //go:embed schemas/plugins.json + pluginSchema string + + //go:embed schemas/aliases.json + aliasSchema string + + //go:embed schemas/views.json + viewsSchema string + + //go:embed schemas/k9s.json + k9sSchema string + + //go:embed schemas/context.json + contextSchema string + + //go:embed schemas/hotkeys.json + hotkeysSchema string + + //go:embed schemas/skin.json + skinSchema string +) + +// Validator tracks schemas validation. +type Validator struct { + schemas map[string]gojsonschema.JSONLoader + loader *gojsonschema.SchemaLoader +} + +// NewValidator returns a new instance. +func NewValidator() *Validator { + v := Validator{ + schemas: map[string]gojsonschema.JSONLoader{ + K9sSchema: gojsonschema.NewStringLoader(k9sSchema), + ContextSchema: gojsonschema.NewStringLoader(contextSchema), + AliasesSchema: gojsonschema.NewStringLoader(aliasSchema), + ViewsSchema: gojsonschema.NewStringLoader(viewsSchema), + PluginsSchema: gojsonschema.NewStringLoader(pluginSchema), + HotkeysSchema: gojsonschema.NewStringLoader(hotkeysSchema), + SkinSchema: gojsonschema.NewStringLoader(skinSchema), + }, + } + v.register() + + return &v +} + +// Init initializes the schemas. +func (v *Validator) register() { + v.loader = gojsonschema.NewSchemaLoader() + v.loader.Validate = true + for k, s := range v.schemas { + if err := v.loader.AddSchema(k, s); err != nil { + log.Error().Err(err).Msgf("schema initialization failed: %q", k) + } + } +} + +// Validate runs document thru given schema validation. +func (v *Validator) Validate(k string, bb []byte) error { + var m interface{} + err := yaml.Unmarshal(bb, &m) + if err != nil { + return err + } + + s, ok := v.schemas[k] + if !ok { + return fmt.Errorf("no schema found for: %q", k) + } + result, err := gojsonschema.Validate(s, gojsonschema.NewGoLoader(m)) + if err != nil { + return err + } + if result.Valid() { + return nil + } + + slices.SortFunc(result.Errors(), func(a, b gojsonschema.ResultError) int { + return cmp.Compare(a.Description(), b.Description()) + }) + var errs error + for _, re := range result.Errors() { + errs = errors.Join(errs, errors.New(re.Description())) + } + + return errs +} + +func (v *Validator) ValidateObj(k string, o any) error { + s, ok := v.schemas[k] + if !ok { + return fmt.Errorf("no schema found for: %q", k) + } + result, err := gojsonschema.Validate(s, gojsonschema.NewGoLoader(o)) + if err != nil { + return err + } + if result.Valid() { + return nil + } + + slices.SortFunc(result.Errors(), func(a, b gojsonschema.ResultError) int { + return cmp.Compare(a.Description(), b.Description()) + }) + var errs error + for _, re := range result.Errors() { + errs = errors.Join(errs, errors.New(re.Description())) + } + + return errs +} diff --git a/internal/config/json/validator_test.go b/internal/config/json/validator_test.go new file mode 100644 index 0000000000..9bab5e0776 --- /dev/null +++ b/internal/config/json/validator_test.go @@ -0,0 +1,223 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package json_test + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/derailed/k9s/internal/config/json" + "github.com/stretchr/testify/assert" +) + +func TestValidatePluginDir(t *testing.T) { + skinDir := "../../../plugins" + ee, err := os.ReadDir(skinDir) + assert.NoError(t, err) + p := json.NewValidator() + for _, e := range ee { + if e.IsDir() { + continue + } + ext := filepath.Ext(e.Name()) + if ext == ".md" { + continue + } + assert.True(t, ext == ".yaml", fmt.Sprintf("expected yaml file: %q", e.Name())) + assert.True(t, !strings.Contains(e.Name(), "_"), fmt.Sprintf("underscore in: %q", e.Name())) + bb, err := os.ReadFile(filepath.Join(skinDir, e.Name())) + assert.NoError(t, err) + assert.NoError(t, p.Validate(json.PluginsSchema, bb), e.Name()) + } +} + +func TestValidateSkinDir(t *testing.T) { + skinDir := "../../../skins" + ee, err := os.ReadDir(skinDir) + assert.NoError(t, err) + p := json.NewValidator() + for _, e := range ee { + if e.IsDir() { + continue + } + ext := filepath.Ext(e.Name()) + assert.True(t, ext == ".yaml", fmt.Sprintf("expected yaml file: %q", e.Name())) + assert.True(t, !strings.Contains(e.Name(), "_"), fmt.Sprintf("underscore in: %q", e.Name())) + bb, err := os.ReadFile(filepath.Join(skinDir, e.Name())) + assert.NoError(t, err) + assert.NoError(t, p.Validate(json.SkinSchema, bb), e.Name()) + } +} + +func TestValidateSkin(t *testing.T) { + uu := map[string]struct { + f string + err string + }{ + "happy": { + f: "testdata/skins/cool.yaml", + }, + "toast": { + f: "testdata/skins/toast.yaml", + err: `Additional property bodys is not allowed`, + }, + } + + v := json.NewValidator() + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + bb, err := os.ReadFile(u.f) + assert.NoError(t, err) + if err := v.Validate(json.SkinSchema, bb); err != nil { + assert.Equal(t, u.err, err.Error()) + } + }) + } +} + +func TestValidateK9s(t *testing.T) { + uu := map[string]struct { + f string + err string + }{ + "happy": { + f: "testdata/k9s/cool.yaml", + }, + "toast": { + f: "testdata/k9s/toast.yaml", + err: `Additional property shellPods is not allowed`, + }, + } + + v := json.NewValidator() + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + bb, err := os.ReadFile(u.f) + assert.NoError(t, err) + if err := v.Validate(json.K9sSchema, bb); err != nil { + assert.Equal(t, u.err, err.Error()) + } + }) + } +} + +func TestValidateContext(t *testing.T) { + uu := map[string]struct { + f string + err string + }{ + "happy": { + f: "testdata/context/cool.yaml", + }, + "toast": { + f: "testdata/context/toast.yaml", + err: `Additional property fred is not allowed +Additional property namespaces is not allowed`, + }, + } + + v := json.NewValidator() + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + bb, err := os.ReadFile(u.f) + assert.NoError(t, err) + if err := v.Validate(json.ContextSchema, bb); err != nil { + assert.Equal(t, u.err, err.Error()) + } + }) + } +} + +func TestValidatePlugins(t *testing.T) { + uu := map[string]struct { + f string + err string + }{ + "happy": { + f: "testdata/plugins/cool.yaml", + }, + "toast": { + f: "testdata/plugins/toast.yaml", + err: `Additional property shortCuts is not allowed +scopes is required +shortCut is required`, + }, + } + + v := json.NewValidator() + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + bb, err := os.ReadFile(u.f) + assert.NoError(t, err) + if err := v.Validate(json.PluginsSchema, bb); err != nil { + assert.Equal(t, u.err, err.Error()) + } + }) + } +} + +func TestValidateAliases(t *testing.T) { + uu := map[string]struct { + f string + err string + }{ + "happy": { + f: "testdata/aliases/cool.yaml", + }, + "toast": { + f: "testdata/aliases/toast.yaml", + err: `Additional property alias is not allowed +aliases is required`, + }, + } + + v := json.NewValidator() + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + bb, err := os.ReadFile(u.f) + assert.NoError(t, err) + if err := v.Validate(json.AliasesSchema, bb); err != nil { + assert.Equal(t, u.err, err.Error()) + } + }) + } +} + +func TestValidateViews(t *testing.T) { + uu := map[string]struct { + f string + err string + }{ + "happy": { + f: "testdata/views/cool.yaml", + }, + "toast": { + f: "testdata/views/toast.yaml", + err: `Additional property cols is not allowed +Additional property sortCol is not allowed +Invalid type. Expected: object, given: null +columns is required`, + }, + } + + v := json.NewValidator() + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + bb, err := os.ReadFile(u.f) + assert.NoError(t, err) + if err := v.Validate(json.ViewsSchema, bb); err != nil { + assert.Equal(t, u.err, err.Error()) + } + }) + } +} diff --git a/internal/config/k9s.go b/internal/config/k9s.go index 4afbb2b111..953fb7ca3b 100644 --- a/internal/config/k9s.go +++ b/internal/config/k9s.go @@ -1,35 +1,36 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config import ( - "github.com/derailed/k9s/internal/client" -) + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + "sync" -const ( - defaultRefreshRate = 2 - defaultMaxConnRetry = 5 + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config/data" + "github.com/rs/zerolog/log" ) // K9s tracks K9s configuration options. type K9s struct { - LiveViewAutoRefresh bool `yaml:"liveViewAutoRefresh"` - RefreshRate int `yaml:"refreshRate"` - MaxConnRetry int `yaml:"maxConnRetry"` - EnableMouse bool `yaml:"enableMouse"` - Headless bool `yaml:"headless"` - Logoless bool `yaml:"logoless"` - Crumbsless bool `yaml:"crumbsless"` - ReadOnly bool `yaml:"readOnly"` - NoExitOnCtrlC bool `yaml:"noExitOnCtrlC"` - NoIcons bool `yaml:"noIcons"` - SkipLatestRevCheck bool `yaml:"skipLatestRevCheck"` - Logger *Logger `yaml:"logger"` - CurrentContext string `yaml:"currentContext"` - CurrentCluster string `yaml:"currentCluster"` - KeepMissingClusters bool `yaml:"keepMissingClusters"` - Clusters map[string]*Cluster `yaml:"clusters,omitempty"` - Thresholds Threshold `yaml:"thresholds"` - ScreenDumpDir string `yaml:"screenDumpDir"` - DisablePodCounting bool `yaml:"disablePodCounting"` + LiveViewAutoRefresh bool `json:"liveViewAutoRefresh" yaml:"liveViewAutoRefresh"` + ScreenDumpDir string `json:"screenDumpDir" yaml:"screenDumpDir,omitempty"` + RefreshRate int `json:"refreshRate" yaml:"refreshRate"` + MaxConnRetry int `json:"maxConnRetry" yaml:"maxConnRetry"` + ReadOnly bool `json:"readOnly" yaml:"readOnly"` + NoExitOnCtrlC bool `json:"noExitOnCtrlC" yaml:"noExitOnCtrlC"` + UI UI `json:"ui" yaml:"ui"` + SkipLatestRevCheck bool `json:"skipLatestRevCheck" yaml:"skipLatestRevCheck"` + DisablePodCounting bool `json:"disablePodCounting" yaml:"disablePodCounting"` + ShellPod ShellPod `json:"shellPod" yaml:"shellPod"` + ImageScans ImageScans `json:"imageScans" yaml:"imageScans"` + Logger Logger `json:"logger" yaml:"logger"` + Thresholds Threshold `json:"thresholds" yaml:"thresholds"` manualRefreshRate int manualHeadless *bool manualLogoless *bool @@ -37,224 +38,303 @@ type K9s struct { manualReadOnly *bool manualCommand *string manualScreenDumpDir *string + dir *data.Dir + activeContextName string + activeConfig *data.Config + conn client.Connection + ks data.KubeSettings + mx sync.RWMutex } // NewK9s create a new K9s configuration. -func NewK9s() *K9s { +func NewK9s(conn client.Connection, ks data.KubeSettings) *K9s { return &K9s{ RefreshRate: defaultRefreshRate, MaxConnRetry: defaultMaxConnRetry, + ScreenDumpDir: AppDumpsDir, Logger: NewLogger(), - Clusters: make(map[string]*Cluster), Thresholds: NewThreshold(), - ScreenDumpDir: K9sDefaultScreenDumpDir, + ShellPod: NewShellPod(), + ImageScans: NewImageScans(), + dir: data.NewDir(AppContextsDir), + conn: conn, + ks: ks, } } -func (k *K9s) CurrentContextDir() string { - return SanitizeFilename(k.CurrentContext) +func (k *K9s) resetConnection(conn client.Connection) { + k.mx.Lock() + defer k.mx.Unlock() + + k.conn = conn } -// ActivateCluster initializes the active cluster is not present. -func (k *K9s) ActivateCluster(ns string) { - if k.Clusters == nil { - k.Clusters = map[string]*Cluster{} +// Save saves the k9s config to disk. +func (k *K9s) Save(force bool) error { + if k.getActiveConfig() == nil { + log.Warn().Msgf("Save failed. no active config detected") + return nil } - if _, ok := k.Clusters[k.CurrentCluster]; ok { - return + path := filepath.Join( + AppContextsDir, + data.SanitizeContextSubpath(k.activeConfig.Context.GetClusterName(), k.getActiveContextName()), + data.MainConfigFile, + ) + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) || force { + return k.dir.Save(path, k.getActiveConfig()) } - cl := NewCluster() - cl.Namespace.Active = ns - k.Clusters[k.CurrentCluster] = cl -} -// OverrideRefreshRate set the refresh rate manually. -func (k *K9s) OverrideRefreshRate(r int) { - k.manualRefreshRate = r + return nil } -// OverrideHeadless toggle the header manually. -func (k *K9s) OverrideHeadless(b bool) { - k.manualHeadless = &b +// Merge merges k9s configs. +func (k *K9s) Merge(k1 *K9s) { + if k1 == nil { + return + } + + k.LiveViewAutoRefresh = k1.LiveViewAutoRefresh + k.ScreenDumpDir = k1.ScreenDumpDir + k.RefreshRate = k1.RefreshRate + k.MaxConnRetry = k1.MaxConnRetry + k.ReadOnly = k1.ReadOnly + k.NoExitOnCtrlC = k1.NoExitOnCtrlC + k.UI = k1.UI + k.SkipLatestRevCheck = k1.SkipLatestRevCheck + k.DisablePodCounting = k1.DisablePodCounting + k.ShellPod = k1.ShellPod + k.Logger = k1.Logger + k.ImageScans = k1.ImageScans + if k1.Thresholds != nil { + k.Thresholds = k1.Thresholds + } } -// OverrideLogoless toggle the k9s logo manually. -func (k *K9s) OverrideLogoless(b bool) { - k.manualLogoless = &b +// AppScreenDumpDir fetch screen dumps dir. +func (k *K9s) AppScreenDumpDir() string { + d := k.ScreenDumpDir + if isStringSet(k.manualScreenDumpDir) { + d = *k.manualScreenDumpDir + k.ScreenDumpDir = d + } + if d == "" { + d = AppDumpsDir + } + + return d } -// OverrideCrumbsless tooh the crumbslessness manually. -func (k *K9s) OverrideCrumbsless(b bool) { - k.manualCrumbsless = &b +// ContextScreenDumpDir fetch context specific screen dumps dir. +func (k *K9s) ContextScreenDumpDir() string { + return filepath.Join(k.AppScreenDumpDir(), k.contextPath()) } -// OverrideReadOnly set the readonly mode manually. -func (k *K9s) OverrideReadOnly(b bool) { - if b { - k.manualReadOnly = &b +func (k *K9s) contextPath() string { + if k.getActiveConfig() == nil { + return "na" } + + return data.SanitizeContextSubpath( + k.getActiveConfig().Context.GetClusterName(), + k.ActiveContextName(), + ) } -// OverrideWrite set the write mode manually. -func (k *K9s) OverrideWrite(b bool) { - if b { - var flag bool - k.manualReadOnly = &flag - } +// Reset resets configuration and context. +func (k *K9s) Reset() { + k.setActiveConfig(nil) + k.setActiveContextName("") } -// OverrideCommand set the command manually. -func (k *K9s) OverrideCommand(cmd string) { - k.manualCommand = &cmd +// ActiveContextNamespace fetch the context active ns. +func (k *K9s) ActiveContextNamespace() (string, error) { + act, err := k.ActiveContext() + if err != nil { + return "", err + } + + return act.Namespace.Active, nil } -// OverrideScreenDumpDir set the screen dump dir manually. -func (k *K9s) OverrideScreenDumpDir(dir string) { - k.manualScreenDumpDir = &dir +// ActiveContextName returns the active context name. +func (k *K9s) ActiveContextName() string { + return k.getActiveContextName() } -// IsHeadless returns headless setting. -func (k *K9s) IsHeadless() bool { - h := k.Headless - if k.manualHeadless != nil && *k.manualHeadless { - h = *k.manualHeadless +// ActiveContext returns the currently active context. +func (k *K9s) ActiveContext() (*data.Context, error) { + if cfg := k.getActiveConfig(); cfg != nil && cfg.Context != nil { + return cfg.Context, nil + } + ct, err := k.ActivateContext(k.getActiveContextName()) + if err != nil { + return nil, err } - return h + return ct, nil } -// IsLogoless returns logoless setting. -func (k *K9s) IsLogoless() bool { - h := k.Logoless - if k.manualLogoless != nil && *k.manualLogoless { - h = *k.manualLogoless - } +func (k *K9s) setActiveConfig(c *data.Config) { + k.mx.Lock() + defer k.mx.Unlock() - return h + k.activeConfig = c } -// IsCrumbsless returns crumbsless setting. -func (k *K9s) IsCrumbsless() bool { - h := k.Crumbsless - if k.manualCrumbsless != nil && *k.manualCrumbsless { - h = *k.manualCrumbsless - } +func (k *K9s) getActiveConfig() *data.Config { + k.mx.RLock() + defer k.mx.RUnlock() - return h + return k.activeConfig } -// GetRefreshRate returns the current refresh rate. -func (k *K9s) GetRefreshRate() int { - rate := k.RefreshRate - if k.manualRefreshRate != 0 { - rate = k.manualRefreshRate - } +func (k *K9s) setActiveContextName(n string) { + k.mx.Lock() + defer k.mx.Unlock() - return rate + k.activeContextName = n } -// IsReadOnly returns the readonly setting. -func (k *K9s) IsReadOnly() bool { - readOnly := k.ReadOnly - if k.manualReadOnly != nil { - readOnly = *k.manualReadOnly - } +func (k *K9s) getActiveContextName() string { + k.mx.RLock() + defer k.mx.RUnlock() - return readOnly + return k.activeContextName } -// ActiveCluster returns the currently active cluster. -func (k *K9s) ActiveCluster() *Cluster { - if k.Clusters == nil { - k.Clusters = map[string]*Cluster{} +// ActivateContext initializes the active context if not present. +func (k *K9s) ActivateContext(n string) (*data.Context, error) { + k.setActiveContextName(n) + ct, err := k.ks.GetContext(n) + if err != nil { + return nil, err + } + + cfg, err := k.dir.Load(n, ct) + if err != nil { + return nil, err + } + k.setActiveConfig(cfg) + + k.Validate(k.conn, k.ks) + // If the context specifies a namespace, use it! + if ns := ct.Namespace; ns != client.BlankNamespace { + k.getActiveConfig().Context.Namespace.Active = ns + } else if k.activeConfig.Context.Namespace.Active == "" { + k.getActiveConfig().Context.Namespace.Active = client.DefaultNamespace } - if c, ok := k.Clusters[k.CurrentCluster]; ok { - return c + if k.getActiveConfig().Context == nil { + return nil, fmt.Errorf("context activation failed for: %s", n) } - k.Clusters[k.CurrentCluster] = NewCluster() - return k.Clusters[k.CurrentCluster] + return k.getActiveConfig().Context, nil } -func (k *K9s) GetScreenDumpDir() string { - screenDumpDir := k.ScreenDumpDir - if k.manualScreenDumpDir != nil && *k.manualScreenDumpDir != "" { - screenDumpDir = *k.manualScreenDumpDir +// Reload reloads the context config from disk. +func (k *K9s) Reload() error { + ct, err := k.ks.GetContext(k.getActiveContextName()) + if err != nil { + return err } - if screenDumpDir == "" { - return K9sDefaultScreenDumpDir + cfg, err := k.dir.Load(k.getActiveContextName(), ct) + if err != nil { + return err } + k.setActiveConfig(cfg) + k.getActiveConfig().Validate(k.conn, k.ks) - return screenDumpDir + return nil } -func (k *K9s) validateDefaults() { - if k.RefreshRate <= 0 { - k.RefreshRate = defaultRefreshRate +// Override overrides k9s config from cli args. +func (k *K9s) Override(k9sFlags *Flags) { + if k9sFlags.RefreshRate != nil && *k9sFlags.RefreshRate != DefaultRefreshRate { + k.manualRefreshRate = *k9sFlags.RefreshRate } - if k.MaxConnRetry <= 0 { - k.MaxConnRetry = defaultMaxConnRetry + + k.manualHeadless = k9sFlags.Headless + k.manualLogoless = k9sFlags.Logoless + k.manualCrumbsless = k9sFlags.Crumbsless + if k9sFlags.ReadOnly != nil && *k9sFlags.ReadOnly { + k.manualReadOnly = k9sFlags.ReadOnly } - if k.ScreenDumpDir == "" { - k.ScreenDumpDir = K9sDefaultScreenDumpDir + if k9sFlags.Write != nil && *k9sFlags.Write { + var false bool + k.manualReadOnly = &false } + k.manualCommand = k9sFlags.Command + k.manualScreenDumpDir = k9sFlags.ScreenDumpDir } -func (k *K9s) validateClusters(c client.Connection, ks KubeSettings) { - cc, err := ks.ClusterNames() - if err != nil { - return +// IsHeadless returns headless setting. +func (k *K9s) IsHeadless() bool { + if isBoolSet(k.manualHeadless) { + return true } - for key, cluster := range k.Clusters { - cluster.Validate(c, ks) - // if the cluster is defined in the $KUBECONFIG file, keep it in the k9s config file - if _, ok := cc[key]; ok { - continue - } - // if we asked to keep the clusters in the config file - if k.KeepMissingClusters { - continue - } + return k.UI.Headless +} - // else remove it from the k9s config file - if k.CurrentCluster == key { - k.CurrentCluster = "" - } - delete(k.Clusters, key) +// IsLogoless returns logoless setting. +func (k *K9s) IsLogoless() bool { + if isBoolSet(k.manualLogoless) { + return true } + + return k.UI.Logoless } -// Validate the current configuration. -func (k *K9s) Validate(c client.Connection, ks KubeSettings) { - k.validateDefaults() - if k.Clusters == nil { - k.Clusters = map[string]*Cluster{} +// IsCrumbsless returns crumbsless setting. +func (k *K9s) IsCrumbsless() bool { + if isBoolSet(k.manualCrumbsless) { + return true } - k.validateClusters(c, ks) - if k.Logger == nil { - k.Logger = NewLogger() - } else { - k.Logger.Validate(c, ks) + return k.UI.Crumbsless +} + +// GetRefreshRate returns the current refresh rate. +func (k *K9s) GetRefreshRate() int { + if k.manualRefreshRate != 0 { + return k.manualRefreshRate } - if k.Thresholds == nil { - k.Thresholds = NewThreshold() + + return k.RefreshRate +} + +// IsReadOnly returns the readonly setting. +func (k *K9s) IsReadOnly() bool { + ro := k.ReadOnly + if cfg := k.getActiveConfig(); cfg != nil && cfg.Context.ReadOnly != nil { + ro = *cfg.Context.ReadOnly } - k.Thresholds.Validate(c, ks) + if k.manualReadOnly != nil { + ro = *k.manualReadOnly + } + + return ro +} - if context, err := ks.CurrentContextName(); err == nil && len(k.CurrentContext) == 0 { - k.CurrentContext = context - k.CurrentCluster = "" +// Validate the current configuration. +func (k *K9s) Validate(c client.Connection, ks data.KubeSettings) { + if k.RefreshRate <= 0 { + k.RefreshRate = defaultRefreshRate + } + if k.MaxConnRetry <= 0 { + k.MaxConnRetry = defaultMaxConnRetry } - if cl, err := ks.CurrentClusterName(); err == nil && len(k.CurrentCluster) == 0 { - k.CurrentCluster = cl + if k.getActiveConfig() == nil { + if n, err := ks.CurrentContextName(); err == nil { + _, _ = k.ActivateContext(n) + } } + k.ShellPod = k.ShellPod.Validate() + k.Logger = k.Logger.Validate() + k.Thresholds = k.Thresholds.Validate() - if _, ok := k.Clusters[k.CurrentCluster]; !ok { - k.Clusters[k.CurrentCluster] = NewCluster() + if cfg := k.getActiveConfig(); cfg != nil { + cfg.Validate(c, ks) } - k.Clusters[k.CurrentCluster].Validate(c, ks) } diff --git a/internal/config/k9s_int_test.go b/internal/config/k9s_int_test.go new file mode 100644 index 0000000000..f950502e57 --- /dev/null +++ b/internal/config/k9s_int_test.go @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_k9sOverrides(t *testing.T) { + var ( + true = true + cmd = "po" + dir = "/tmp/blee" + ) + + uu := map[string]struct { + k *K9s + rate int + ro, hl, cl, ll bool + }{ + "plain": { + k: &K9s{ + LiveViewAutoRefresh: false, + ScreenDumpDir: "", + RefreshRate: 10, + MaxConnRetry: 0, + ReadOnly: false, + NoExitOnCtrlC: false, + UI: UI{}, + SkipLatestRevCheck: false, + DisablePodCounting: false, + }, + rate: 10, + }, + "set": { + k: &K9s{ + LiveViewAutoRefresh: false, + ScreenDumpDir: "", + RefreshRate: 10, + MaxConnRetry: 0, + ReadOnly: true, + NoExitOnCtrlC: false, + UI: UI{ + Headless: true, + Logoless: true, + Crumbsless: true, + }, + SkipLatestRevCheck: false, + DisablePodCounting: false, + }, + rate: 10, + ro: true, + hl: true, + ll: true, + cl: true, + }, + "overrides": { + k: &K9s{ + LiveViewAutoRefresh: false, + ScreenDumpDir: "", + RefreshRate: 10, + MaxConnRetry: 0, + ReadOnly: false, + NoExitOnCtrlC: false, + UI: UI{ + Headless: false, + Logoless: false, + Crumbsless: false, + }, + SkipLatestRevCheck: false, + DisablePodCounting: false, + manualRefreshRate: 100, + manualReadOnly: &true, + manualHeadless: &true, + manualLogoless: &true, + manualCrumbsless: &true, + manualCommand: &cmd, + manualScreenDumpDir: &dir, + }, + rate: 100, + ro: true, + hl: true, + ll: true, + cl: true, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.rate, u.k.GetRefreshRate()) + assert.Equal(t, u.ro, u.k.IsReadOnly()) + assert.Equal(t, u.cl, u.k.IsCrumbsless()) + assert.Equal(t, u.hl, u.k.IsHeadless()) + assert.Equal(t, u.ll, u.k.IsLogoless()) + + }) + } +} + +func Test_screenDumpDirOverride(t *testing.T) { + uu := map[string]struct { + dir string + e string + }{ + "empty": { + e: "/tmp/k9s-test/screen-dumps", + }, + "override": { + dir: "/tmp/k9s-test/sd", + e: "/tmp/k9s-test/sd", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + cfg := NewConfig(nil) + assert.NoError(t, cfg.Load("testdata/configs/k9s.yaml", true)) + + cfg.K9s.manualScreenDumpDir = &u.dir + assert.Equal(t, u.e, cfg.K9s.AppScreenDumpDir()) + }) + } +} diff --git a/internal/config/k9s_test.go b/internal/config/k9s_test.go index 631af2228c..d74c59c39d 100644 --- a/internal/config/k9s_test.go +++ b/internal/config/k9s_test.go @@ -1,168 +1,148 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config_test import ( + "errors" "testing" "github.com/derailed/k9s/internal/config" - m "github.com/petergtz/pegomock" + "github.com/derailed/k9s/internal/config/mock" "github.com/stretchr/testify/assert" + "k8s.io/cli-runtime/pkg/genericclioptions" ) -func TestIsReadOnly(t *testing.T) { +func TestK9sReload(t *testing.T) { + config.AppConfigDir = "/tmp/k9s-test" + + cl, ct := "cl-1", "ct-1-1" + uu := map[string]struct { - config string - read, write bool - readOnly bool + k *config.K9s + cl, ct string + err error }{ - "writable": { - config: "k9s.yml", - }, - "writable_read_override": { - config: "k9s.yml", - read: true, - readOnly: true, - }, - "writable_write_override": { - config: "k9s.yml", - write: true, + "no-context": { + k: config.NewK9s( + mock.NewMockConnection(), + mock.NewMockKubeSettings(&genericclioptions.ConfigFlags{ + ClusterName: &cl, + Context: &ct, + }), + ), + err: errors.New(`no context found for: ""`), }, - "readonly": { - config: "k9s_readonly.yml", - readOnly: true, - }, - "readonly_read_override": { - config: "k9s_readonly.yml", - read: true, - readOnly: true, - }, - "readonly_write_override": { - config: "k9s_readonly.yml", - write: true, - }, - "readonly_both_override": { - config: "k9s_readonly.yml", - read: true, - write: true, + "set-context": { + k: config.NewK9s( + mock.NewMockConnection(), + mock.NewMockKubeSettings(&genericclioptions.ConfigFlags{ + ClusterName: &cl, + Context: &ct, + }), + ), + ct: "ct-1-1", + cl: "cl-1", }, } - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Nil(t, cfg.Load("testdata/"+u.config)) - cfg.K9s.OverrideReadOnly(u.read) - cfg.K9s.OverrideWrite(u.write) - assert.Equal(t, u.readOnly, cfg.K9s.IsReadOnly()) + _, _ = u.k.ActivateContext(u.ct) + assert.Equal(t, u.err, u.k.Reload()) + ct, err := u.k.ActiveContext() + assert.Equal(t, u.err, err) + if err == nil { + assert.Equal(t, u.cl, ct.ClusterName) + } }) } } -func TestK9sValidate(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - - mk := NewMockKubeSettings() - m.When(mk.CurrentContextName()).ThenReturn("ctx1", nil) - m.When(mk.CurrentClusterName()).ThenReturn("c1", nil) - m.When(mk.ClusterNames()).ThenReturn(map[string]struct{}{"c1": {}, "c2": {}}, nil) - m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"default"}) - - c := config.NewK9s() - c.Validate(mc, mk) - - assert.Equal(t, 2, c.RefreshRate) - assert.Equal(t, int64(100), c.Logger.TailCount) - assert.Equal(t, 5000, c.Logger.BufferSize) - assert.Equal(t, "ctx1", c.CurrentContext) - assert.Equal(t, "c1", c.CurrentCluster) - assert.Equal(t, 1, len(c.Clusters)) - assert.Equal(t, config.K9sDefaultScreenDumpDir, c.GetScreenDumpDir()) - _, ok := c.Clusters[c.CurrentCluster] - assert.True(t, ok) -} - -func TestK9sValidateBlank(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - - mk := NewMockKubeSettings() - m.When(mk.CurrentContextName()).ThenReturn("ctx1", nil) - m.When(mk.CurrentClusterName()).ThenReturn("c1", nil) - m.When(mk.ClusterNames()).ThenReturn(map[string]struct{}{"c1": {}, "c2": {}}, nil) - m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"default"}) - - var c config.K9s - c.Validate(mc, mk) - - assert.Equal(t, 2, c.RefreshRate) - assert.Equal(t, int64(100), c.Logger.TailCount) - assert.Equal(t, 5000, c.Logger.BufferSize) - assert.Equal(t, "ctx1", c.CurrentContext) - assert.Equal(t, "c1", c.CurrentCluster) - assert.Equal(t, 1, len(c.Clusters)) - _, ok := c.Clusters[c.CurrentCluster] - assert.True(t, ok) -} - -func TestK9sActiveClusterZero(t *testing.T) { - c := config.NewK9s() - c.CurrentCluster = "fred" - cl := c.ActiveCluster() - assert.NotNil(t, cl) - assert.Equal(t, "default", cl.Namespace.Active) - assert.Equal(t, 1, len(cl.Namespace.Favorites)) -} +func TestK9sMerge(t *testing.T) { + cl, ct := "cl-1", "ct-1-1" -func TestK9sActiveClusterBlank(t *testing.T) { - var c config.K9s - cl := c.ActiveCluster() - assert.Equal(t, config.NewCluster(), cl) -} - -func TestK9sActiveCluster(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("testdata/k9s.yml")) - - cl := cfg.K9s.ActiveCluster() - assert.NotNil(t, cl) - assert.Equal(t, "kube-system", cl.Namespace.Active) - assert.Equal(t, 5, len(cl.Namespace.Favorites)) -} - -func TestGetScreenDumpDir(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("testdata/k9s.yml")) - - assert.Equal(t, "/tmp", cfg.K9s.GetScreenDumpDir()) -} - -func TestGetScreenDumpDirOverride(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("testdata/k9s.yml")) - cfg.K9s.OverrideScreenDumpDir("/override") + uu := map[string]struct { + k1, k2 *config.K9s + ek *config.K9s + }{ + "no-opt": { + k1: config.NewK9s( + mock.NewMockConnection(), + mock.NewMockKubeSettings(&genericclioptions.ConfigFlags{ + ClusterName: &cl, + Context: &ct, + }), + ), + ek: config.NewK9s( + mock.NewMockConnection(), + mock.NewMockKubeSettings(&genericclioptions.ConfigFlags{ + ClusterName: &cl, + Context: &ct, + }), + ), + }, + "override": { + k1: &config.K9s{ + LiveViewAutoRefresh: false, + ScreenDumpDir: "", + RefreshRate: 0, + MaxConnRetry: 0, + ReadOnly: false, + NoExitOnCtrlC: false, + UI: config.UI{}, + SkipLatestRevCheck: false, + DisablePodCounting: false, + ShellPod: config.ShellPod{}, + ImageScans: config.ImageScans{}, + Logger: config.Logger{}, + Thresholds: nil, + }, + k2: &config.K9s{ + LiveViewAutoRefresh: true, + MaxConnRetry: 100, + ShellPod: config.NewShellPod(), + }, + ek: &config.K9s{ + LiveViewAutoRefresh: true, + ScreenDumpDir: "", + RefreshRate: 0, + MaxConnRetry: 100, + ReadOnly: false, + NoExitOnCtrlC: false, + UI: config.UI{}, + SkipLatestRevCheck: false, + DisablePodCounting: false, + ShellPod: config.NewShellPod(), + ImageScans: config.ImageScans{}, + Logger: config.Logger{}, + Thresholds: nil, + }, + }, + } - assert.Equal(t, "/override", cfg.K9s.GetScreenDumpDir()) + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + u.k1.Merge(u.k2) + assert.Equal(t, u.ek, u.k1) + }) + } } -func TestGetScreenDumpDirOverrideEmpty(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("testdata/k9s.yml")) - cfg.K9s.OverrideScreenDumpDir("") +func TestContextScreenDumpDir(t *testing.T) { + cfg := mock.NewMockConfig() + _, err := cfg.K9s.ActivateContext("ct-1-1") - assert.Equal(t, "/tmp", cfg.K9s.GetScreenDumpDir()) + assert.NoError(t, err) + assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true)) + assert.Equal(t, "/tmp/k9s-test/screen-dumps/cl-1/ct-1-1", cfg.K9s.ContextScreenDumpDir()) } -func TestGetScreenDumpDirEmpty(t *testing.T) { - mk := NewMockKubeSettings() - cfg := config.NewConfig(mk) - assert.Nil(t, cfg.Load("testdata/k9s1.yml")) - cfg.K9s.OverrideScreenDumpDir("") +func TestAppScreenDumpDir(t *testing.T) { + cfg := mock.NewMockConfig() - assert.Equal(t, config.K9sDefaultScreenDumpDir, cfg.K9s.GetScreenDumpDir()) + assert.Nil(t, cfg.Load("testdata/configs/k9s.yaml", true)) + assert.Equal(t, "/tmp/k9s-test/screen-dumps", cfg.K9s.AppScreenDumpDir()) } diff --git a/internal/config/logger.go b/internal/config/logger.go index a80f29e8d0..1b199a71fe 100644 --- a/internal/config/logger.go +++ b/internal/config/logger.go @@ -1,32 +1,32 @@ -package config +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s -import ( - "github.com/derailed/k9s/internal/client" -) +package config const ( // DefaultLoggerTailCount tracks default log tail size. DefaultLoggerTailCount = 100 + // MaxLogThreshold sets the max value for log size. MaxLogThreshold = 5000 + // DefaultSinceSeconds tracks default log age. - DefaultSinceSeconds = 300 // all logs + DefaultSinceSeconds = -1 // tail logs by default ) // Logger tracks logger options. type Logger struct { - TailCount int64 `yaml:"tail"` - BufferSize int `yaml:"buffer"` - SinceSeconds int64 `yaml:"sinceSeconds"` - FullScreenLogs bool `yaml:"fullScreenLogs"` - TextWrap bool `yaml:"textWrap"` - ShowTime bool `yaml:"showTime"` - ShowJSON bool `yaml:"showJSON"` + TailCount int64 `json:"tail" yaml:"tail"` + BufferSize int `json:"buffer" yaml:"buffer"` + SinceSeconds int64 `json:"sinceSeconds" yaml:"sinceSeconds"` + TextWrap bool `json:"textWrap" yaml:"textWrap"` + ShowTime bool `json:"showTime" yaml:"showTime"` + ShowJSON bool `json:"showJSON" yaml:"showJSON"` } // NewLogger returns a new instance. -func NewLogger() *Logger { - return &Logger{ +func NewLogger() Logger { + return Logger{ TailCount: DefaultLoggerTailCount, BufferSize: MaxLogThreshold, SinceSeconds: DefaultSinceSeconds, @@ -34,7 +34,7 @@ func NewLogger() *Logger { } // Validate checks thresholds and make sure we're cool. If not use defaults. -func (l *Logger) Validate(_ client.Connection, _ KubeSettings) { +func (l Logger) Validate() Logger { if l.TailCount <= 0 { l.TailCount = DefaultLoggerTailCount } @@ -47,4 +47,6 @@ func (l *Logger) Validate(_ client.Connection, _ KubeSettings) { if l.SinceSeconds == 0 { l.SinceSeconds = DefaultSinceSeconds } + + return l } diff --git a/internal/config/logger_test.go b/internal/config/logger_test.go index ff1d2ff11a..753466f459 100644 --- a/internal/config/logger_test.go +++ b/internal/config/logger_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config_test import ( @@ -9,7 +12,7 @@ import ( func TestNewLogger(t *testing.T) { l := config.NewLogger() - l.Validate(nil, nil) + l = l.Validate() assert.Equal(t, int64(100), l.TailCount) assert.Equal(t, 5000, l.BufferSize) @@ -17,7 +20,7 @@ func TestNewLogger(t *testing.T) { func TestLoggerValidate(t *testing.T) { var l config.Logger - l.Validate(nil, nil) + l = l.Validate() assert.Equal(t, int64(100), l.TailCount) assert.Equal(t, 5000, l.BufferSize) diff --git a/internal/config/mock/test_helpers.go b/internal/config/mock/test_helpers.go new file mode 100644 index 0000000000..eae5709d43 --- /dev/null +++ b/internal/config/mock/test_helpers.go @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package mock + +import ( + "errors" + "fmt" + "io/fs" + "os" + "strings" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config" + version "k8s.io/apimachinery/pkg/version" + "k8s.io/cli-runtime/pkg/genericclioptions" + disk "k8s.io/client-go/discovery/cached/disk" + dynamic "k8s.io/client-go/dynamic" + kubernetes "k8s.io/client-go/kubernetes" + restclient "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd/api" + versioned "k8s.io/metrics/pkg/client/clientset/versioned" +) + +func EnsureDir(d string) error { + if _, err := os.Stat(d); errors.Is(err, fs.ErrNotExist) { + return os.MkdirAll(d, 0700) + } + if err := os.RemoveAll(d); err != nil { + return err + } + + return os.MkdirAll(d, 0700) +} + +func NewMockConfig() *config.Config { + config.AppContextsDir = "/tmp/test" + cl, ct := "cl-1", "ct-1-1" + flags := genericclioptions.ConfigFlags{ + ClusterName: &cl, + Context: &ct, + } + cfg := config.NewConfig( + NewMockKubeSettings(&flags), + ) + + return cfg +} + +type mockKubeSettings struct { + flags *genericclioptions.ConfigFlags + cts map[string]*api.Context +} + +func NewMockKubeSettings(f *genericclioptions.ConfigFlags) mockKubeSettings { + _, idx, _ := strings.Cut(*f.ClusterName, "-") + ctId := "ct-" + idx + + return mockKubeSettings{ + flags: f, + cts: map[string]*api.Context{ + ctId + "-1": { + Cluster: *f.ClusterName, + Namespace: "", + }, + ctId + "-2": { + Cluster: *f.ClusterName, + Namespace: "ns-2", + }, + ctId + "-3": { + Cluster: *f.ClusterName, + Namespace: client.DefaultNamespace, + }, + "fred-blee": { + Cluster: "arn:aws:eks:eu-central-1:xxx:cluster/fred-blee", + Namespace: client.DefaultNamespace, + }, + }, + } +} +func (m mockKubeSettings) CurrentContextName() (string, error) { + return *m.flags.Context, nil +} +func (m mockKubeSettings) CurrentClusterName() (string, error) { + return *m.flags.ClusterName, nil +} +func (m mockKubeSettings) CurrentNamespaceName() (string, error) { + return "default", nil +} +func (m mockKubeSettings) GetContext(s string) (*api.Context, error) { + ct, ok := m.cts[s] + if !ok { + return nil, fmt.Errorf("no context found for: %q", s) + } + return ct, nil +} +func (m mockKubeSettings) CurrentContext() (*api.Context, error) { + return m.GetContext(*m.flags.Context) +} +func (m mockKubeSettings) ContextNames() (map[string]struct{}, error) { + mm := make(map[string]struct{}, len(m.cts)) + for k := range m.cts { + mm[k] = struct{}{} + } + + return mm, nil +} + +type mockConnection struct { + ct string +} + +func NewMockConnection() mockConnection { + return mockConnection{} +} +func NewMockConnectionWithContext(ct string) mockConnection { + return mockConnection{ct: ct} +} + +func (m mockConnection) CanI(ns, gvr, n string, verbs []string) (bool, error) { + return true, nil +} +func (m mockConnection) Config() *client.Config { + return nil +} +func (m mockConnection) ConnectionOK() bool { + return false +} +func (m mockConnection) Dial() (kubernetes.Interface, error) { + return nil, nil +} +func (m mockConnection) DialLogs() (kubernetes.Interface, error) { + return nil, nil +} +func (m mockConnection) SwitchContext(ctx string) error { + return nil +} +func (m mockConnection) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { + return nil, nil +} +func (m mockConnection) RestConfig() (*restclient.Config, error) { + return nil, nil +} +func (m mockConnection) MXDial() (*versioned.Clientset, error) { + return nil, nil +} +func (m mockConnection) DynDial() (dynamic.Interface, error) { + return nil, nil +} +func (m mockConnection) HasMetrics() bool { + return false +} +func (m mockConnection) ValidNamespaceNames() (client.NamespaceNames, error) { + return nil, nil +} +func (m mockConnection) IsValidNamespace(string) bool { + return true +} +func (m mockConnection) ServerVersion() (*version.Info, error) { + return nil, nil +} +func (m mockConnection) CheckConnectivity() bool { + return false +} +func (m mockConnection) ActiveContext() string { + return m.ct +} +func (m mockConnection) ActiveNamespace() string { + return "" +} +func (m mockConnection) IsActiveNamespace(string) bool { + return false +} diff --git a/internal/config/mock_connection_test.go b/internal/config/mock_connection_test.go deleted file mode 100644 index 2a024947ca..0000000000 --- a/internal/config/mock_connection_test.go +++ /dev/null @@ -1,671 +0,0 @@ -// Code generated by pegomock. DO NOT EDIT. -// Source: github.com/derailed/k9s/internal/client (interfaces: Connection) - -package config_test - -import ( - client "github.com/derailed/k9s/internal/client" - pegomock "github.com/petergtz/pegomock" - v1 "k8s.io/api/core/v1" - version "k8s.io/apimachinery/pkg/version" - disk "k8s.io/client-go/discovery/cached/disk" - dynamic "k8s.io/client-go/dynamic" - kubernetes "k8s.io/client-go/kubernetes" - rest "k8s.io/client-go/rest" - versioned "k8s.io/metrics/pkg/client/clientset/versioned" - "reflect" - "time" -) - -type MockConnection struct { - fail func(message string, callerSkip ...int) -} - -func NewMockConnection(options ...pegomock.Option) *MockConnection { - mock := &MockConnection{} - for _, option := range options { - option.Apply(mock) - } - return mock -} - -func (mock *MockConnection) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } -func (mock *MockConnection) FailHandler() pegomock.FailHandler { return mock.fail } - -func (mock *MockConnection) ActiveCluster() string { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ActiveCluster", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()}) - var ret0 string - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - } - return ret0 -} - -func (mock *MockConnection) ActiveNamespace() string { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ActiveNamespace", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()}) - var ret0 string - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - } - return ret0 -} - -func (mock *MockConnection) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CachedDiscovery", params, []reflect.Type{reflect.TypeOf((**disk.CachedDiscoveryClient)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *disk.CachedDiscoveryClient - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*disk.CachedDiscoveryClient) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) CanI(_param0 string, _param1 string, _param2 []string) (bool, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0, _param1, _param2} - result := pegomock.GetGenericMockFrom(mock).Invoke("CanI", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 bool - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) CheckConnectivity() bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CheckConnectivity", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockConnection) Config() *client.Config { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("Config", params, []reflect.Type{reflect.TypeOf((**client.Config)(nil)).Elem()}) - var ret0 *client.Config - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*client.Config) - } - } - return ret0 -} - -func (mock *MockConnection) ConnectionOK() bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ConnectionOK", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockConnection) Dial() (kubernetes.Interface, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("Dial", params, []reflect.Type{reflect.TypeOf((*kubernetes.Interface)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 kubernetes.Interface - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(kubernetes.Interface) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) DialLogs() (kubernetes.Interface, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("DialLogs", params, []reflect.Type{reflect.TypeOf((*kubernetes.Interface)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 kubernetes.Interface - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(kubernetes.Interface) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) DynDial() (dynamic.Interface, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("DynDial", params, []reflect.Type{reflect.TypeOf((*dynamic.Interface)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 dynamic.Interface - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(dynamic.Interface) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) HasMetrics() bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("HasMetrics", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockConnection) IsActiveNamespace(_param0 string) bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("IsActiveNamespace", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockConnection) MXDial() (*versioned.Clientset, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("MXDial", params, []reflect.Type{reflect.TypeOf((**versioned.Clientset)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *versioned.Clientset - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*versioned.Clientset) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) RestConfig() (*rest.Config, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("RestConfig", params, []reflect.Type{reflect.TypeOf((**rest.Config)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *rest.Config - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*rest.Config) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) ServerVersion() (*version.Info, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ServerVersion", params, []reflect.Type{reflect.TypeOf((**version.Info)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *version.Info - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*version.Info) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) SwitchContext(_param0 string) error { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("SwitchContext", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(error) - } - } - return ret0 -} - -func (mock *MockConnection) ValidNamespaces() ([]v1.Namespace, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ValidNamespaces", params, []reflect.Type{reflect.TypeOf((*[]v1.Namespace)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 []v1.Namespace - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].([]v1.Namespace) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) VerifyWasCalledOnce() *VerifierMockConnection { - return &VerifierMockConnection{ - mock: mock, - invocationCountMatcher: pegomock.Times(1), - } -} - -func (mock *MockConnection) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockConnection { - return &VerifierMockConnection{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - } -} - -func (mock *MockConnection) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockConnection { - return &VerifierMockConnection{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - inOrderContext: inOrderContext, - } -} - -func (mock *MockConnection) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockConnection { - return &VerifierMockConnection{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - timeout: timeout, - } -} - -type VerifierMockConnection struct { - mock *MockConnection - invocationCountMatcher pegomock.Matcher - inOrderContext *pegomock.InOrderContext - timeout time.Duration -} - -func (verifier *VerifierMockConnection) ActiveCluster() *MockConnection_ActiveCluster_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ActiveCluster", params, verifier.timeout) - return &MockConnection_ActiveCluster_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ActiveCluster_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ActiveCluster_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ActiveCluster_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) ActiveNamespace() *MockConnection_ActiveNamespace_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ActiveNamespace", params, verifier.timeout) - return &MockConnection_ActiveNamespace_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ActiveNamespace_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ActiveNamespace_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ActiveNamespace_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) CachedDiscovery() *MockConnection_CachedDiscovery_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CachedDiscovery", params, verifier.timeout) - return &MockConnection_CachedDiscovery_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CachedDiscovery_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CachedDiscovery_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_CachedDiscovery_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) CanI(_param0 string, _param1 string, _param2 []string) *MockConnection_CanI_OngoingVerification { - params := []pegomock.Param{_param0, _param1, _param2} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CanI", params, verifier.timeout) - return &MockConnection_CanI_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CanI_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CanI_OngoingVerification) GetCapturedArguments() (string, string, []string) { - _param0, _param1, _param2 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1], _param2[len(_param2)-1] -} - -func (c *MockConnection_CanI_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 [][]string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(string) - } - _param1 = make([]string, len(c.methodInvocations)) - for u, param := range params[1] { - _param1[u] = param.(string) - } - _param2 = make([][]string, len(c.methodInvocations)) - for u, param := range params[2] { - _param2[u] = param.([]string) - } - } - return -} - -func (verifier *VerifierMockConnection) CheckConnectivity() *MockConnection_CheckConnectivity_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CheckConnectivity", params, verifier.timeout) - return &MockConnection_CheckConnectivity_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CheckConnectivity_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CheckConnectivity_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_CheckConnectivity_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) Config() *MockConnection_Config_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Config", params, verifier.timeout) - return &MockConnection_Config_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_Config_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_Config_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_Config_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) ConnectionOK() *MockConnection_ConnectionOK_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ConnectionOK", params, verifier.timeout) - return &MockConnection_ConnectionOK_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ConnectionOK_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ConnectionOK_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ConnectionOK_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) Dial() *MockConnection_Dial_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Dial", params, verifier.timeout) - return &MockConnection_Dial_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_Dial_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_Dial_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_Dial_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) DynDial() *MockConnection_DynDial_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DynDial", params, verifier.timeout) - return &MockConnection_DynDial_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_DynDial_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_DynDial_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_DynDial_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) HasMetrics() *MockConnection_HasMetrics_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasMetrics", params, verifier.timeout) - return &MockConnection_HasMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_HasMetrics_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_HasMetrics_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_HasMetrics_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) IsActiveNamespace(_param0 string) *MockConnection_IsActiveNamespace_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "IsActiveNamespace", params, verifier.timeout) - return &MockConnection_IsActiveNamespace_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_IsActiveNamespace_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_IsActiveNamespace_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockConnection_IsActiveNamespace_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockConnection) MXDial() *MockConnection_MXDial_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "MXDial", params, verifier.timeout) - return &MockConnection_MXDial_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_MXDial_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_MXDial_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_MXDial_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) RestConfig() *MockConnection_RestConfig_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RestConfig", params, verifier.timeout) - return &MockConnection_RestConfig_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_RestConfig_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_RestConfig_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_RestConfig_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) ServerVersion() *MockConnection_ServerVersion_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ServerVersion", params, verifier.timeout) - return &MockConnection_ServerVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ServerVersion_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ServerVersion_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ServerVersion_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) SwitchContext(_param0 string) *MockConnection_SwitchContext_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SwitchContext", params, verifier.timeout) - return &MockConnection_SwitchContext_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_SwitchContext_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_SwitchContext_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockConnection_SwitchContext_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockConnection) ValidNamespaces() *MockConnection_ValidNamespaces_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ValidNamespaces", params, verifier.timeout) - return &MockConnection_ValidNamespaces_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ValidNamespaces_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ValidNamespaces_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ValidNamespaces_OngoingVerification) GetAllCapturedArguments() { -} diff --git a/internal/config/mock_kubesettings_test.go b/internal/config/mock_kubesettings_test.go deleted file mode 100644 index 9269469f91..0000000000 --- a/internal/config/mock_kubesettings_test.go +++ /dev/null @@ -1,249 +0,0 @@ -// Code generated by pegomock. DO NOT EDIT. -// Source: github.com/derailed/k9s/internal/config (interfaces: KubeSettings) - -package config_test - -import ( - pegomock "github.com/petergtz/pegomock" - v1 "k8s.io/api/core/v1" - "reflect" - "time" -) - -type MockKubeSettings struct { - fail func(message string, callerSkip ...int) -} - -func NewMockKubeSettings(options ...pegomock.Option) *MockKubeSettings { - mock := &MockKubeSettings{} - for _, option := range options { - option.Apply(mock) - } - return mock -} - -func (mock *MockKubeSettings) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } -func (mock *MockKubeSettings) FailHandler() pegomock.FailHandler { return mock.fail } - -func (mock *MockKubeSettings) ClusterNames() (map[string]struct{}, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockKubeSettings().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ClusterNames", params, []reflect.Type{reflect.TypeOf((*map[string]struct{})(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 map[string]struct{} - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(map[string]struct{}) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockKubeSettings) CurrentClusterName() (string, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockKubeSettings().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CurrentClusterName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockKubeSettings) CurrentContextName() (string, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockKubeSettings().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CurrentContextName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockKubeSettings) CurrentNamespaceName() (string, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockKubeSettings().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CurrentNamespaceName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockKubeSettings) NamespaceNames(_param0 []v1.Namespace) []string { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockKubeSettings().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("NamespaceNames", params, []reflect.Type{reflect.TypeOf((*[]string)(nil)).Elem()}) - var ret0 []string - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].([]string) - } - } - return ret0 -} - -func (mock *MockKubeSettings) VerifyWasCalledOnce() *VerifierMockKubeSettings { - return &VerifierMockKubeSettings{ - mock: mock, - invocationCountMatcher: pegomock.Times(1), - } -} - -func (mock *MockKubeSettings) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockKubeSettings { - return &VerifierMockKubeSettings{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - } -} - -func (mock *MockKubeSettings) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockKubeSettings { - return &VerifierMockKubeSettings{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - inOrderContext: inOrderContext, - } -} - -func (mock *MockKubeSettings) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockKubeSettings { - return &VerifierMockKubeSettings{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - timeout: timeout, - } -} - -type VerifierMockKubeSettings struct { - mock *MockKubeSettings - invocationCountMatcher pegomock.Matcher - inOrderContext *pegomock.InOrderContext - timeout time.Duration -} - -func (verifier *VerifierMockKubeSettings) ClusterNames() *MockKubeSettings_ClusterNames_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ClusterNames", params, verifier.timeout) - return &MockKubeSettings_ClusterNames_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockKubeSettings_ClusterNames_OngoingVerification struct { - mock *MockKubeSettings - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockKubeSettings_ClusterNames_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockKubeSettings_ClusterNames_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockKubeSettings) CurrentClusterName() *MockKubeSettings_CurrentClusterName_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CurrentClusterName", params, verifier.timeout) - return &MockKubeSettings_CurrentClusterName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockKubeSettings_CurrentClusterName_OngoingVerification struct { - mock *MockKubeSettings - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockKubeSettings_CurrentClusterName_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockKubeSettings_CurrentClusterName_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockKubeSettings) CurrentContextName() *MockKubeSettings_CurrentContextName_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CurrentContextName", params, verifier.timeout) - return &MockKubeSettings_CurrentContextName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockKubeSettings_CurrentContextName_OngoingVerification struct { - mock *MockKubeSettings - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockKubeSettings_CurrentContextName_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockKubeSettings_CurrentContextName_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockKubeSettings) CurrentNamespaceName() *MockKubeSettings_CurrentNamespaceName_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CurrentNamespaceName", params, verifier.timeout) - return &MockKubeSettings_CurrentNamespaceName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockKubeSettings_CurrentNamespaceName_OngoingVerification struct { - mock *MockKubeSettings - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockKubeSettings_CurrentNamespaceName_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockKubeSettings_CurrentNamespaceName_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockKubeSettings) NamespaceNames(_param0 []v1.Namespace) *MockKubeSettings_NamespaceNames_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "NamespaceNames", params, verifier.timeout) - return &MockKubeSettings_NamespaceNames_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockKubeSettings_NamespaceNames_OngoingVerification struct { - mock *MockKubeSettings - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockKubeSettings_NamespaceNames_OngoingVerification) GetCapturedArguments() []v1.Namespace { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockKubeSettings_NamespaceNames_OngoingVerification) GetAllCapturedArguments() (_param0 [][]v1.Namespace) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([][]v1.Namespace, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.([]v1.Namespace) - } - } - return -} diff --git a/internal/config/ns_test.go b/internal/config/ns_test.go deleted file mode 100644 index 3348936651..0000000000 --- a/internal/config/ns_test.go +++ /dev/null @@ -1,88 +0,0 @@ -package config_test - -import ( - "fmt" - "testing" - - "github.com/derailed/k9s/internal/config" - m "github.com/petergtz/pegomock" - "github.com/stretchr/testify/assert" -) - -func TestNSValidate(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - mk := NewMockKubeSettings() - m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"ns1", "ns2", "default"}) - - ns := config.NewNamespace() - ns.Validate(mc, mk) - - mk.VerifyWasCalledOnce() - assert.Equal(t, "default", ns.Active) - assert.Equal(t, []string{"default"}, ns.Favorites) -} - -func TestNSValidateMissing(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - mk := NewMockKubeSettings() - - ns := config.NewNamespace() - ns.Validate(mc, mk) - - assert.Equal(t, "default", ns.Active) - assert.Equal(t, []string{"default"}, ns.Favorites) -} - -func TestNSValidateNoNS(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), fmt.Errorf("Crap!")) - mk := NewMockKubeSettings() - m.When(mk.NamespaceNames(namespaces())).ThenReturn([]string{"ns1", "ns2"}) - - ns := config.NewNamespace() - ns.Validate(mc, mk) - - mk.VerifyWasCalledOnce() - assert.Equal(t, "default", ns.Active) - assert.Equal(t, []string{"default"}, ns.Favorites) -} - -func TestNSSetActive(t *testing.T) { - allNS := []string{"ns4", "ns3", "ns2", "ns1", "all", "default"} - uu := []struct { - ns string - fav []string - }{ - {"all", []string{"all", "default"}}, - {"ns1", []string{"ns1", "all", "default"}}, - {"ns2", []string{"ns2", "ns1", "all", "default"}}, - {"ns3", []string{"ns3", "ns2", "ns1", "all", "default"}}, - {"ns4", allNS}, - } - - mk := NewMockKubeSettings() - m.When(mk.NamespaceNames(namespaces())).ThenReturn(allNS) - - ns := config.NewNamespace() - for _, u := range uu { - err := ns.SetActive(u.ns, mk) - - assert.Nil(t, err) - assert.Equal(t, u.ns, ns.Active) - assert.Equal(t, u.fav, ns.Favorites) - } -} - -func TestNSValidateRmFavs(t *testing.T) { - mc := NewMockConnection() - m.When(mc.ValidNamespaces()).ThenReturn(namespaces(), nil) - mk := NewMockKubeSettings() - - ns := config.NewNamespace() - ns.Favorites = []string{"default", "fred", "blee"} - ns.Validate(mc, mk) - - assert.Equal(t, []string{"default", "fred"}, ns.Favorites) -} diff --git a/internal/config/plugin.go b/internal/config/plugin.go index 029bda8995..c676f87fad 100644 --- a/internal/config/plugin.go +++ b/internal/config/plugin.go @@ -1,35 +1,43 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config import ( + "errors" "fmt" + "io/fs" "os" "path/filepath" "strings" + "github.com/derailed/k9s/internal/config/data" + "github.com/derailed/k9s/internal/config/json" + "github.com/adrg/xdg" - "github.com/rs/zerolog/log" "gopkg.in/yaml.v2" ) -// K9sPluginsFilePath manages K9s plugins. -var K9sPluginsFilePath = filepath.Join(K9sHome(), "plugin.yml") -var K9sPluginDirectory = filepath.Join("k9s", "plugins") +const k9sPluginsDir = "k9s/plugins" // Plugins represents a collection of plugins. type Plugins struct { - Plugin map[string]Plugin `yaml:"plugin"` + Plugins map[string]Plugin `yaml:"plugins"` } // Plugin describes a K9s plugin. type Plugin struct { - Scopes []string `yaml:"scopes"` - Args []string `yaml:"args"` - ShortCut string `yaml:"shortCut"` - Pipes []string `yaml:"pipes"` - Description string `yaml:"description"` - Command string `yaml:"command"` - Confirm bool `yaml:"confirm"` - Background bool `yaml:"background"` + Scopes []string `yaml:"scopes"` + Args []string `yaml:"args"` + ShortCut string `yaml:"shortCut"` + Override bool `yaml:"override"` + Pipes []string `yaml:"pipes"` + Description string `yaml:"description"` + Command string `yaml:"command"` + Confirm bool `yaml:"confirm"` + Background bool `yaml:"background"` + Dangerous bool `yaml:"dangerous"` + OverwriteOutput bool `yaml:"overwriteOutput"` } func (p Plugin) String() string { @@ -39,60 +47,73 @@ func (p Plugin) String() string { // NewPlugins returns a new plugin. func NewPlugins() Plugins { return Plugins{ - Plugin: make(map[string]Plugin), + Plugins: make(map[string]Plugin), } } // Load K9s plugins. -func (p Plugins) Load() error { - var pluginDirs []string +func (p Plugins) Load(path string) error { + var errs error + + if err := p.load(AppPluginsFile); err != nil { + errs = errors.Join(errs, err) + } + if err := p.load(path); err != nil { + errs = errors.Join(errs, err) + } + for _, dataDir := range xdg.DataDirs { - pluginDirs = append(pluginDirs, filepath.Join(dataDir, K9sPluginDirectory)) + if err := p.loadPluginDir(filepath.Join(dataDir, k9sPluginsDir)); err != nil { + errs = errors.Join(errs, err) + } } - return p.LoadPlugins(K9sPluginsFilePath, pluginDirs) + + return errs } -// LoadPlugins loads plugins from a given file and a set of plugin directories. -func (p Plugins) LoadPlugins(path string, pluginDirs []string) error { - f, err := os.ReadFile(path) +func (p Plugins) loadPluginDir(dir string) error { + pluginFiles, err := os.ReadDir(dir) if err != nil { - return err - } - var pp Plugins - if err := yaml.Unmarshal(f, &pp); err != nil { - return err + return nil } - for _, pluginDir := range pluginDirs { - pluginFiles, err := os.ReadDir(pluginDir) - if err != nil { - log.Warn().Msgf("Failed reading plugin path %s; %s", pluginDir, err) + var errs error + for _, file := range pluginFiles { + if file.IsDir() || !isYamlFile(file.Name()) { continue } - for _, file := range pluginFiles { - if file.IsDir() || !isYamlFile(file) { - continue - } - pluginFile, err := os.ReadFile(filepath.Join(pluginDir, file.Name())) - if err != nil { - return err - } - var plugin Plugin - if err = yaml.Unmarshal(pluginFile, &plugin); err != nil { - return err - } - p.Plugin[strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))] = plugin + bb, err := os.ReadFile(filepath.Join(dir, file.Name())) + if err != nil { + errs = errors.Join(errs, err) } + var plugin Plugin + if err = yaml.Unmarshal(bb, &plugin); err != nil { + return err + } + p.Plugins[strings.TrimSuffix(file.Name(), filepath.Ext(file.Name()))] = plugin } - for k, v := range pp.Plugin { - p.Plugin[k] = v + return errs +} + +func (p *Plugins) load(path string) error { + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { + return nil + } + bb, err := os.ReadFile(path) + if err != nil { + return err + } + if err := data.JSONValidator.Validate(json.PluginsSchema, bb); err != nil { + return fmt.Errorf("validation failed for %q: %w", path, err) + } + var pp Plugins + if err := yaml.Unmarshal(bb, &pp); err != nil { + return err + } + for k, v := range pp.Plugins { + p.Plugins[k] = v } return nil } - -func isYamlFile(file os.DirEntry) bool { - ext := filepath.Ext(file.Name()) - return ext == ".yml" || ext == ".yaml" -} diff --git a/internal/config/plugin_test.go b/internal/config/plugin_test.go index 8350625e67..1634760eff 100644 --- a/internal/config/plugin_test.go +++ b/internal/config/plugin_test.go @@ -1,13 +1,17 @@ -package config_test +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package config import ( + "os" "testing" - "github.com/derailed/k9s/internal/config" + "github.com/adrg/xdg" "github.com/stretchr/testify/assert" ) -var pluginYmlTestData = config.Plugin{ +var pluginYmlTestData = Plugin{ Scopes: []string{"po", "dp"}, Args: []string{"-n", "$NAMESPACE", "-boolean"}, ShortCut: "shift-s", @@ -17,50 +21,68 @@ var pluginYmlTestData = config.Plugin{ Background: false, } -var test1YmlTestData = config.Plugin{ - Scopes: []string{"po", "dp"}, - Args: []string{"-n", "$NAMESPACE", "-boolean"}, - ShortCut: "shift-s", - Description: "blee", - Command: "duh", - Confirm: true, - Background: false, +var test1YmlTestData = Plugin{ + Scopes: []string{"po", "dp"}, + Args: []string{"-n", "$NAMESPACE", "-boolean"}, + ShortCut: "shift-s", + Description: "blee", + Command: "duh", + Confirm: true, + Background: false, + OverwriteOutput: true, +} + +var test2YmlTestData = Plugin{ + Scopes: []string{"svc", "ing"}, + Args: []string{"-n", "$NAMESPACE", "-oyaml"}, + ShortCut: "shift-r", + Description: "bla", + Command: "duha", + Confirm: false, + Background: true, + OverwriteOutput: false, } -var test2YmlTestData = config.Plugin{ - Scopes: []string{"svc", "ing"}, - Args: []string{"-n", "$NAMESPACE", "-oyaml"}, - ShortCut: "shift-r", - Description: "bla", - Command: "duha", - Confirm: false, - Background: true, +func TestPluginLoad(t *testing.T) { + AppPluginsFile = "/tmp/k9s-test/fred.yaml" + os.Setenv("XDG_DATA_HOME", "/tmp/k9s-test") + xdg.Reload() + + p := NewPlugins() + assert.NoError(t, p.Load("testdata/plugins.yaml")) + + assert.Equal(t, 1, len(p.Plugins)) + k, ok := p.Plugins["blah"] + assert.True(t, ok) + assert.ObjectsAreEqual(pluginYmlTestData, k) } func TestSinglePluginFileLoad(t *testing.T) { - p := config.NewPlugins() - assert.Nil(t, p.LoadPlugins("testdata/plugin.yml", []string{"/random/dir/not/exist"})) + p := NewPlugins() + assert.NoError(t, p.load("testdata/plugins.yaml")) + assert.NoError(t, p.loadPluginDir("/random/dir/not/exist")) - assert.Equal(t, 1, len(p.Plugin)) - k, ok := p.Plugin["blah"] + assert.Equal(t, 1, len(p.Plugins)) + k, ok := p.Plugins["blah"] assert.True(t, ok) assert.ObjectsAreEqual(pluginYmlTestData, k) } func TestMultiplePluginFilesLoad(t *testing.T) { - p := config.NewPlugins() - assert.Nil(t, p.LoadPlugins("testdata/plugin.yml", []string{"testdata/plugins"})) + p := NewPlugins() + assert.NoError(t, p.load("testdata/plugins.yaml")) + assert.NoError(t, p.loadPluginDir("testdata/plugins")) - testPlugins := map[string]config.Plugin{ + testPlugins := map[string]Plugin{ "blah": pluginYmlTestData, "test1": test1YmlTestData, "test2": test2YmlTestData, } - assert.Equal(t, len(testPlugins), len(p.Plugin)) + assert.Equal(t, len(testPlugins), len(p.Plugins)) for name, expectedPlugin := range testPlugins { - k, ok := p.Plugin[name] + k, ok := p.Plugins[name] assert.True(t, ok) assert.ObjectsAreEqual(expectedPlugin, k) } diff --git a/internal/config/scans.go b/internal/config/scans.go new file mode 100644 index 0000000000..31aa8ef788 --- /dev/null +++ b/internal/config/scans.go @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package config + +// Labels tracks a collection of labels. +type Labels map[string][]string + +func (l Labels) exclude(k, val string) bool { + vv, ok := l[k] + if !ok { + return false + } + + for _, v := range vv { + if v == val { + return true + } + } + + return false +} + +// ScanExcludes tracks vul scan exclusions. +type ScanExcludes struct { + Namespaces []string `json:"namespaces" yaml:"namespaces"` + Labels Labels `json:"labels" yaml:"labels"` +} + +func newScanExcludes() ScanExcludes { + return ScanExcludes{ + Labels: make(Labels), + } +} + +func (b ScanExcludes) exclude(ns string, ll map[string]string) bool { + for _, nss := range b.Namespaces { + if nss == ns { + return true + } + } + for k, v := range ll { + if b.Labels.exclude(k, v) { + return true + } + } + + return false +} + +// ImageScans tracks vul scans options. +type ImageScans struct { + Enable bool `json:"enable" yaml:"enable"` + Exclusions ScanExcludes `json:"exclusions" yaml:"exclusions"` +} + +// NewImageScans returns a new instance. +func NewImageScans() ImageScans { + return ImageScans{ + Exclusions: newScanExcludes(), + } +} + +// ShouldExclude checks if scan should be excluder given ns/labels +func (i ImageScans) ShouldExclude(ns string, ll map[string]string) bool { + if !i.Enable { + return false + } + + return i.Exclusions.exclude(ns, ll) +} diff --git a/internal/config/scans_test.go b/internal/config/scans_test.go new file mode 100644 index 0000000000..d410393c10 --- /dev/null +++ b/internal/config/scans_test.go @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package config_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/stretchr/testify/assert" +) + +func TestScansShouldExclude(t *testing.T) { + uu := map[string]struct { + sc config.ImageScans + ns string + ll map[string]string + e bool + }{ + "empty": { + sc: config.NewImageScans(), + }, + "exclude-ns": { + sc: config.ImageScans{ + Enable: true, + Exclusions: config.ScanExcludes{ + Namespaces: []string{"ns-1", "ns-2", "ns-3"}, + Labels: config.Labels{ + "app": []string{"fred", "blee"}, + }, + }, + }, + ns: "ns-1", + ll: map[string]string{ + "app": "freddy", + }, + e: true, + }, + "include-ns": { + sc: config.ImageScans{ + Enable: true, + Exclusions: config.ScanExcludes{ + Namespaces: []string{"ns-1", "ns-2", "ns-3"}, + Labels: config.Labels{ + "app": []string{"fred", "blee"}, + }, + }, + }, + ns: "ns-4", + ll: map[string]string{ + "app": "bozo", + }, + }, + "exclude-labels": { + sc: config.ImageScans{ + Enable: true, + Exclusions: config.ScanExcludes{ + Namespaces: []string{"ns-1", "ns-2", "ns-3"}, + Labels: config.Labels{ + "app": []string{"fred", "blee"}, + }, + }, + }, + ns: "ns-4", + ll: map[string]string{ + "app": "fred", + }, + e: true, + }, + "include-labels": { + sc: config.ImageScans{ + Enable: true, + Exclusions: config.ScanExcludes{ + Namespaces: []string{"ns-1", "ns-2", "ns-3"}, + Labels: config.Labels{ + "app": []string{"fred", "blee"}, + }, + }, + }, + ns: "ns-4", + ll: map[string]string{ + "app": "freddy", + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.sc.ShouldExclude(u.ns, u.ll)) + }) + } +} diff --git a/internal/config/shell_pod.go b/internal/config/shell_pod.go index a9c429b832..08540f2c94 100644 --- a/internal/config/shell_pod.go +++ b/internal/config/shell_pod.go @@ -1,7 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config import ( - "github.com/derailed/k9s/internal/client" v1 "k8s.io/api/core/v1" ) @@ -12,17 +14,20 @@ type Limits map[v1.ResourceName]string // ShellPod represents k9s shell configuration. type ShellPod struct { - Image string `json:"image"` - Command []string `json:"command,omitempty"` - Args []string `json:"args,omitempty"` - Namespace string `json:"namespace"` - Limits Limits `json:"resources,omitempty"` - Labels map[string]string `json:"labels,omitempty"` + Image string `json:"image" yaml:"image"` + Command []string `json:"command,omitempty" yaml:"command,omitempty"` + Args []string `json:"args,omitempty" yaml:"args,omitempty"` + Namespace string `json:"namespace" yaml:"namespace"` + Limits Limits `json:"limits,omitempty" yaml:"limits,omitempty"` + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` + ImagePullSecrets []v1.LocalObjectReference `json:"imagePullSecrets,omitempty" yaml:"imagePullSecrets,omitempty"` + ImagePullPolicy v1.PullPolicy `json:"imagePullPolicy,omitempty" yaml:"imagePullPolicy,omitempty"` + TTY bool `json:"tty,omitempty" yaml:"tty,omitempty"` } // NewShellPod returns a new instance. -func NewShellPod() *ShellPod { - return &ShellPod{ +func NewShellPod() ShellPod { + return ShellPod{ Image: defaultDockerShellImage, Namespace: "default", Limits: defaultLimits(), @@ -30,13 +35,15 @@ func NewShellPod() *ShellPod { } // Validate validates the configuration. -func (s *ShellPod) Validate(client.Connection, KubeSettings) { +func (s ShellPod) Validate() ShellPod { if s.Image == "" { s.Image = defaultDockerShellImage } if len(s.Limits) == 0 { s.Limits = defaultLimits() } + + return s } func defaultLimits() Limits { diff --git a/internal/config/styles.go b/internal/config/styles.go index 9e1194165c..604cfee938 100644 --- a/internal/config/styles.go +++ b/internal/config/styles.go @@ -1,18 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config import ( "fmt" "os" - "path/filepath" + "github.com/derailed/k9s/internal/config/data" + "github.com/derailed/k9s/internal/config/json" "github.com/derailed/tcell/v2" "github.com/derailed/tview" "gopkg.in/yaml.v2" ) -// K9sStylesFile represents K9s skins file location. -var K9sStylesFile = filepath.Join(K9sHome(), "skin.yml") - // StyleListener represents a skin's listener. type StyleListener interface { // StylesChanged notifies listener the skin changed. @@ -20,258 +21,201 @@ type StyleListener interface { } type ( - // Color represents a color. - Color string - - // Colors tracks multiple colors. - Colors []Color - // Styles tracks K9s styling options. Styles struct { - K9s Style `yaml:"k9s"` + K9s Style `json:"k9s" yaml:"k9s"` listeners []StyleListener } // Style tracks K9s styles. Style struct { - Body Body `yaml:"body"` - Prompt Prompt `yaml:"prompt"` - Help Help `yaml:"help"` - Frame Frame `yaml:"frame"` - Info Info `yaml:"info"` - Views Views `yaml:"views"` - Dialog Dialog `yaml:"dialog"` + Body Body `json:"body" yaml:"body"` + Prompt Prompt `json:"prompt" yaml:"prompt"` + Help Help `json:"help" yaml:"help"` + Frame Frame `json:"frame" yaml:"frame"` + Info Info `json:"info" yaml:"info"` + Views Views `json:"views" yaml:"views"` + Dialog Dialog `json:"dialog" yaml:"dialog"` } // Prompt tracks command styles Prompt struct { - FgColor Color `yaml:"fgColor"` - BgColor Color `yaml:"bgColor"` - SuggestColor Color `yaml:"suggestColor"` - Border PromptBorder `yaml:"border"` + FgColor Color `json:"fgColor" yaml:"fgColor"` + BgColor Color `json:"bgColor" yaml:"bgColor"` + SuggestColor Color `json:"" yaml:"suggestColor"` + Border PromptBorder `json:"" yaml:"border"` } // PromptBorder tracks the color of the prompt depending on its kind (e.g., command or filter) PromptBorder struct { - CommandColor Color `yaml:"command"` - DefaultColor Color `yaml:"default"` + CommandColor Color `json:"command" yaml:"command"` + DefaultColor Color `json:"default" yaml:"default"` } // Help tracks help styles. Help struct { - FgColor Color `yaml:"fgColor"` - BgColor Color `yaml:"bgColor"` - SectionColor Color `yaml:"sectionColor"` - KeyColor Color `yaml:"keyColor"` - NumKeyColor Color `yaml:"numKeyColor"` + FgColor Color `json:"fgColor" yaml:"fgColor"` + BgColor Color `json:"bgColor" yaml:"bgColor"` + SectionColor Color `json:"sectionColor" yaml:"sectionColor"` + KeyColor Color `json:"keyColor" yaml:"keyColor"` + NumKeyColor Color `json:"numKeyColor" yaml:"numKeyColor"` } // Body tracks body styles. Body struct { - FgColor Color `yaml:"fgColor"` - BgColor Color `yaml:"bgColor"` - LogoColor Color `yaml:"logoColor"` - LogoColorMsg Color `yaml:"logoColorMsg"` - LogoColorInfo Color `yaml:"logoColorInfo"` - LogoColorWarn Color `yaml:"logoColorWarn"` - LogoColorError Color `yaml:"logoColorError"` + FgColor Color `json:"fgColor" yaml:"fgColor"` + BgColor Color `json:"bgColor" yaml:"bgColor"` + LogoColor Color `json:"logoColor" yaml:"logoColor"` + LogoColorMsg Color `json:"logoColorMsg" yaml:"logoColorMsg"` + LogoColorInfo Color `json:"logoColorInfo" yaml:"logoColorInfo"` + LogoColorWarn Color `json:"logoColorWarn" yaml:"logoColorWarn"` + LogoColorError Color `json:"logoColorError" yaml:"logoColorError"` } // Dialog tracks dialog styles. Dialog struct { - FgColor Color `yaml:"fgColor"` - BgColor Color `yaml:"bgColor"` - ButtonFgColor Color `yaml:"buttonFgColor"` - ButtonBgColor Color `yaml:"buttonBgColor"` - ButtonFocusFgColor Color `yaml:"buttonFocusFgColor"` - ButtonFocusBgColor Color `yaml:"buttonFocusBgColor"` - LabelFgColor Color `yaml:"labelFgColor"` - FieldFgColor Color `yaml:"fieldFgColor"` + FgColor Color `json:"fgColor" yaml:"fgColor"` + BgColor Color `json:"bgColor" yaml:"bgColor"` + ButtonFgColor Color `json:"buttonFgColor" yaml:"buttonFgColor"` + ButtonBgColor Color `json:"buttonBgColor" yaml:"buttonBgColor"` + ButtonFocusFgColor Color `json:"buttonFocusFgColor" yaml:"buttonFocusFgColor"` + ButtonFocusBgColor Color `json:"buttonFocusBgColor" yaml:"buttonFocusBgColor"` + LabelFgColor Color `json:"labelFgColor" yaml:"labelFgColor"` + FieldFgColor Color `json:"fieldFgColor" yaml:"fieldFgColor"` } // Frame tracks frame styles. Frame struct { - Title Title `yaml:"title"` - Border Border `yaml:"border"` - Menu Menu `yaml:"menu"` - Crumb Crumb `yaml:"crumbs"` - Status Status `yaml:"status"` + Title Title `json:"title" yaml:"title"` + Border Border `json:"border" yaml:"border"` + Menu Menu `json:"menu" yaml:"menu"` + Crumb Crumb `json:"crumbs" yaml:"crumbs"` + Status Status `json:"status" yaml:"status"` } // Views tracks individual view styles. Views struct { - Table Table `yaml:"table"` - Xray Xray `yaml:"xray"` - Charts Charts `yaml:"charts"` - Yaml Yaml `yaml:"yaml"` - Picker Picker `yaml:"picker"` - Log Log `yaml:"logs"` + Table Table `json:"table" yaml:"table"` + Xray Xray `json:"xray" yaml:"xray"` + Charts Charts `json:"charts" yaml:"charts"` + Yaml Yaml `json:"yaml" yaml:"yaml"` + Picker Picker `json:"picker" yaml:"picker"` + Log Log `json:"logs" yaml:"logs"` } // Status tracks resource status styles. Status struct { - NewColor Color `yaml:"newColor"` - ModifyColor Color `yaml:"modifyColor"` - AddColor Color `yaml:"addColor"` - PendingColor Color `yaml:"pendingColor"` - ErrorColor Color `yaml:"errorColor"` - HighlightColor Color `yaml:"highlightColor"` - KillColor Color `yaml:"killColor"` - CompletedColor Color `yaml:"completedColor"` + NewColor Color `json:"newColor" yaml:"newColor"` + ModifyColor Color `json:"modifyColor" yaml:"modifyColor"` + AddColor Color `json:"addColor" yaml:"addColor"` + PendingColor Color `json:"pendingColor" yaml:"pendingColor"` + ErrorColor Color `json:"errorColor" yaml:"errorColor"` + HighlightColor Color `json:"highlightColor" yaml:"highlightColor"` + KillColor Color `json:"killColor" yaml:"killColor"` + CompletedColor Color `json:"completedColor" yaml:"completedColor"` } // Log tracks Log styles. Log struct { - FgColor Color `yaml:"fgColor"` - BgColor Color `yaml:"bgColor"` - Indicator LogIndicator `yaml:"indicator"` + FgColor Color `json:"fgColor" yaml:"fgColor"` + BgColor Color `json:"bgColor" yaml:"bgColor"` + Indicator LogIndicator `json:"indicator" yaml:"indicator"` } // Picker tracks color when selecting containers Picker struct { - MainColor Color `yaml:"mainColor"` - FocusColor Color `yaml:"focusColor"` - ShortcutColor Color `yaml:"shortcutColor"` + MainColor Color `json:"mainColor" yaml:"mainColor"` + FocusColor Color `json:"focusColor" yaml:"focusColor"` + ShortcutColor Color `json:"shortcutColor" yaml:"shortcutColor"` } // LogIndicator tracks log view indicator. LogIndicator struct { - FgColor Color `yaml:"fgColor"` - BgColor Color `yaml:"bgColor"` - ToggleOnColor Color `yaml:"toggleOnColor"` - ToggleOffColor Color `yaml:"toggleOffColor"` + FgColor Color `json:"fgColor" yaml:"fgColor"` + BgColor Color `json:"bgColor" yaml:"bgColor"` + ToggleOnColor Color `json:"toggleOnColor" yaml:"toggleOnColor"` + ToggleOffColor Color `json:"toggleOffColor" yaml:"toggleOffColor"` } // Yaml tracks yaml styles. Yaml struct { - KeyColor Color `yaml:"keyColor"` - ValueColor Color `yaml:"valueColor"` - ColonColor Color `yaml:"colonColor"` + KeyColor Color `json:"keyColor" yaml:"keyColor"` + ValueColor Color `json:"valueColor" yaml:"valueColor"` + ColonColor Color `json:"colonColor" yaml:"colonColor"` } // Title tracks title styles. Title struct { - FgColor Color `yaml:"fgColor"` - BgColor Color `yaml:"bgColor"` - HighlightColor Color `yaml:"highlightColor"` - CounterColor Color `yaml:"counterColor"` - FilterColor Color `yaml:"filterColor"` + FgColor Color `json:"fgColor" yaml:"fgColor"` + BgColor Color `json:"bgColor" yaml:"bgColor"` + HighlightColor Color `json:"highlightColor" yaml:"highlightColor"` + CounterColor Color `json:"counterColor" yaml:"counterColor"` + FilterColor Color `json:"filterColor" yaml:"filterColor"` } // Info tracks info styles. Info struct { - SectionColor Color `yaml:"sectionColor"` - FgColor Color `yaml:"fgColor"` + SectionColor Color `json:"sectionColor" yaml:"sectionColor"` + FgColor Color `json:"fgColor" yaml:"fgColor"` } // Border tracks border styles. Border struct { - FgColor Color `yaml:"fgColor"` - FocusColor Color `yaml:"focusColor"` + FgColor Color `json:"fgColor" yaml:"fgColor"` + FocusColor Color `json:"focusColor" yaml:"focusColor"` } // Crumb tracks crumbs styles. Crumb struct { - FgColor Color `yaml:"fgColor"` - BgColor Color `yaml:"bgColor"` - ActiveColor Color `yaml:"activeColor"` + FgColor Color `json:"fgColor" yaml:"fgColor"` + BgColor Color `json:"bgColor" yaml:"bgColor"` + ActiveColor Color `json:"activeColor" yaml:"activeColor"` } // Table tracks table styles. Table struct { - FgColor Color `yaml:"fgColor"` - BgColor Color `yaml:"bgColor"` - CursorFgColor Color `yaml:"cursorFgColor"` - CursorBgColor Color `yaml:"cursorBgColor"` - MarkColor Color `yaml:"markColor"` - Header TableHeader `yaml:"header"` + FgColor Color `json:"fgColor" yaml:"fgColor"` + BgColor Color `json:"bgColor" yaml:"bgColor"` + CursorFgColor Color `json:"cursorFgColor" yaml:"cursorFgColor"` + CursorBgColor Color `json:"cursorBgColor" yaml:"cursorBgColor"` + MarkColor Color `json:"markColor" yaml:"markColor"` + Header TableHeader `json:"header" yaml:"header"` } // TableHeader tracks table header styles. TableHeader struct { - FgColor Color `yaml:"fgColor"` - BgColor Color `yaml:"bgColor"` - SorterColor Color `yaml:"sorterColor"` + FgColor Color `json:"fgColor" yaml:"fgColor"` + BgColor Color `json:"bgColor" yaml:"bgColor"` + SorterColor Color `json:"sorterColor" yaml:"sorterColor"` } // Xray tracks xray styles. Xray struct { - FgColor Color `yaml:"fgColor"` - BgColor Color `yaml:"bgColor"` - CursorColor Color `yaml:"cursorColor"` - CursorTextColor Color `yaml:"cursorTextColor"` - GraphicColor Color `yaml:"graphicColor"` + FgColor Color `json:"fgColor" yaml:"fgColor"` + BgColor Color `json:"bgColor" yaml:"bgColor"` + CursorColor Color `json:"cursorColor" yaml:"cursorColor"` + CursorTextColor Color `json:"cursorTextColor" yaml:"cursorTextColor"` + GraphicColor Color `json:"graphicColor" yaml:"graphicColor"` } // Menu tracks menu styles. Menu struct { - FgColor Color `yaml:"fgColor"` - KeyColor Color `yaml:"keyColor"` - NumKeyColor Color `yaml:"numKeyColor"` + FgColor Color `json:"fgColor" yaml:"fgColor"` + KeyColor Color `json:"keyColor" yaml:"keyColor"` + NumKeyColor Color `json:"numKeyColor" yaml:"numKeyColor"` } // Charts tracks charts styles. Charts struct { - BgColor Color `yaml:"bgColor"` - DialBgColor Color `yaml:"dialBgColor"` - ChartBgColor Color `yaml:"chartBgColor"` - DefaultDialColors Colors `yaml:"defaultDialColors"` - DefaultChartColors Colors `yaml:"defaultChartColors"` - ResourceColors map[string]Colors `yaml:"resourceColors"` + BgColor Color `json:"bgColor" yaml:"bgColor"` + DialBgColor Color `json:"dialBgColor" yaml:"dialBgColor"` + ChartBgColor Color `json:"chartBgColor" yaml:"chartBgColor"` + DefaultDialColors Colors `json:"defaultDialColors" yaml:"defaultDialColors"` + DefaultChartColors Colors `json:"defaultChartColors" yaml:"defaultChartColors"` + ResourceColors map[string]Colors `json:"resourceColors" yaml:"resourceColors"` } ) -const ( - // DefaultColor represents a default color. - DefaultColor Color = "default" - - // TransparentColor represents the terminal bg color. - TransparentColor Color = "-" -) - -// NewColor returns a new color. -func NewColor(c string) Color { - return Color(c) -} - -// String returns color as string. -func (c Color) String() string { - if c.isHex() { - return string(c) - } - if c == DefaultColor { - return "-" - } - col := c.Color().TrueColor().Hex() - if col < 0 { - return "-" - } - - return fmt.Sprintf("#%06x", col) -} - -func (c Color) isHex() bool { - return len(c) == 7 && c[0] == '#' -} - -// Color returns a view color. -func (c Color) Color() tcell.Color { - if c == DefaultColor { - return tcell.ColorDefault - } - - return tcell.GetColor(string(c)).TrueColor() -} - -// Colors converts series string colors to colors. -func (c Colors) Colors() []tcell.Color { - cc := make([]tcell.Color, 0, len(c)) - for _, color := range c { - cc = append(cc, color.Color()) - } - return cc -} - func newStyle() Style { return Style{ Body: newBody(), @@ -483,6 +427,11 @@ func newMenu() Menu { // NewStyles creates a new default config. func NewStyles() *Styles { + var s Styles + if err := yaml.Unmarshal(stockSkinTpl, &s); err == nil { + return &s + } + return &Styles{ K9s: newStyle(), } @@ -490,12 +439,9 @@ func NewStyles() *Styles { // Reset resets styles. func (s *Styles) Reset() { - s.K9s = newStyle() -} - -// DefaultSkin loads the default skin. -func (s *Styles) DefaultSkin() { - s.K9s = newStyle() + if err := yaml.Unmarshal(stockSkinTpl, s); err != nil { + s.K9s = newStyle() + } } // FgColor returns the foreground color. @@ -586,15 +532,16 @@ func (s *Styles) Views() Views { // Load K9s configuration from file. func (s *Styles) Load(path string) error { - f, err := os.ReadFile(path) + bb, err := os.ReadFile(path) if err != nil { return err } - - if err := yaml.Unmarshal(f, s); err != nil { + if err := data.JSONValidator.Validate(json.SkinSchema, bb); err != nil { + return err + } + if err := yaml.Unmarshal(bb, s); err != nil { return err } - // s.fireStylesChanged() return nil } @@ -616,3 +563,9 @@ func (s *Styles) Update() { s.fireStylesChanged() } + +// Dump for debug. +func (s *Styles) Dump() { + bb, _ := yaml.Marshal(s) + fmt.Println(string(bb)) +} diff --git a/internal/config/styles_int_test.go b/internal/config/styles_int_test.go new file mode 100644 index 0000000000..1845b1f4ca --- /dev/null +++ b/internal/config/styles_int_test.go @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_newStyle(t *testing.T) { + s := newStyle() + + assert.Equal(t, Color("black"), s.Body.BgColor) + assert.Equal(t, Color("cadetblue"), s.Body.FgColor) + assert.Equal(t, Color("lightskyblue"), s.Frame.Status.NewColor) +} diff --git a/internal/config/styles_test.go b/internal/config/styles_test.go index e934e880b3..b2ada27848 100644 --- a/internal/config/styles_test.go +++ b/internal/config/styles_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config_test import ( @@ -9,6 +12,14 @@ import ( "github.com/stretchr/testify/assert" ) +func TestNewStyle(t *testing.T) { + s := config.NewStyles() + + assert.Equal(t, config.Color("black"), s.K9s.Body.BgColor) + assert.Equal(t, config.Color("cadetblue"), s.K9s.Body.FgColor) + assert.Equal(t, config.Color("lightskyblue"), s.K9s.Frame.Status.NewColor) +} + func TestColor(t *testing.T) { uu := map[string]tcell.Color{ "blah": tcell.ColorDefault, @@ -25,22 +36,9 @@ func TestColor(t *testing.T) { } } -func TestSkinNone(t *testing.T) { - s := config.NewStyles() - assert.Nil(t, s.Load("testdata/empty_skin.yml")) - s.Update() - - assert.Equal(t, "#5f9ea0", s.Body().FgColor.String()) - assert.Equal(t, "#000000", s.Body().BgColor.String()) - assert.Equal(t, "#000000", s.Table().BgColor.String()) - assert.Equal(t, tcell.ColorCadetBlue.TrueColor(), s.FgColor()) - assert.Equal(t, tcell.ColorBlack.TrueColor(), s.BgColor()) - assert.Equal(t, tcell.ColorBlack.TrueColor(), tview.Styles.PrimitiveBackgroundColor) -} - -func TestSkin(t *testing.T) { +func TestSkinHappy(t *testing.T) { s := config.NewStyles() - assert.Nil(t, s.Load("testdata/black_and_wtf.yml")) + assert.Nil(t, s.Load("../../skins/black-and-wtf.yaml")) s.Update() assert.Equal(t, "#ffffff", s.Body().FgColor.String()) @@ -51,12 +49,38 @@ func TestSkin(t *testing.T) { assert.Equal(t, tcell.ColorBlack.TrueColor(), tview.Styles.PrimitiveBackgroundColor) } -func TestSkinNotExits(t *testing.T) { - s := config.NewStyles() - assert.NotNil(t, s.Load("testdata/blee.yml")) -} +func TestSkinLoad(t *testing.T) { + uu := map[string]struct { + f string + err string + }{ + "not-exist": { + f: "testdata/skins/blee.yaml", + err: "open testdata/skins/blee.yaml: no such file or directory", + }, + "toast": { + f: "testdata/skins/boarked.yaml", + err: `Additional property bgColor is not allowed +Additional property fgColor is not allowed +Additional property logoColor is not allowed +Invalid type. Expected: object, given: array`, + }, + } -func TestSkinBoarked(t *testing.T) { - s := config.NewStyles() - assert.NotNil(t, s.Load("testdata/skin_boarked.yml")) + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + s := config.NewStyles() + err := s.Load(u.f) + if err != nil { + assert.Equal(t, u.err, err.Error()) + } + assert.Equal(t, "#5f9ea0", s.Body().FgColor.String()) + assert.Equal(t, "#000000", s.Body().BgColor.String()) + assert.Equal(t, "#000000", s.Table().BgColor.String()) + assert.Equal(t, tcell.ColorCadetBlue.TrueColor(), s.FgColor()) + assert.Equal(t, tcell.ColorBlack.TrueColor(), s.BgColor()) + assert.Equal(t, tcell.ColorBlack.TrueColor(), tview.Styles.PrimitiveBackgroundColor) + }) + } } diff --git a/internal/config/templates/aliases.yaml b/internal/config/templates/aliases.yaml new file mode 100644 index 0000000000..ee4d9ec014 --- /dev/null +++ b/internal/config/templates/aliases.yaml @@ -0,0 +1,9 @@ +aliases: + dp: deployments + sec: v1/secrets + jo: jobs + cr: clusterroles + crb: clusterrolebindings + ro: roles + rb: rolebindings + np: networkpolicies diff --git a/internal/config/templates/benchmarks.yaml b/internal/config/templates/benchmarks.yaml new file mode 100644 index 0000000000..9efba4cf3e --- /dev/null +++ b/internal/config/templates/benchmarks.yaml @@ -0,0 +1,4 @@ +benchmarks: + defaults: + concurrency: 2 + requests: 200 \ No newline at end of file diff --git a/internal/config/templates/hotkeys.yaml b/internal/config/templates/hotkeys.yaml new file mode 100644 index 0000000000..5c2723b673 --- /dev/null +++ b/internal/config/templates/hotkeys.yaml @@ -0,0 +1,6 @@ +hotKeys: + # Examples... + # shift-0: + # shortCut: Shift-0 + # description: View Workloads + # command: wk k8s-app=cilium \ No newline at end of file diff --git a/internal/config/templates/stock-skin.yaml b/internal/config/templates/stock-skin.yaml new file mode 100644 index 0000000000..3d677bf89f --- /dev/null +++ b/internal/config/templates/stock-skin.yaml @@ -0,0 +1,116 @@ +# ----------------------------------------------------------------------------- +# Stock skin +# ----------------------------------------------------------------------------- + +# Skin... +k9s: + body: + fgColor: cadetblue + bgColor: black + logoColor: orange + logoColorMsg: white + logoColorInfo: green + logoColorWarn: mediumvioletred + logoColorError: red + prompt: + fgColor: cadetblue + bgColor: black + suggestColor: dodgerblue + border: + command: aqua + default: seagreen + help: + fgColor: cadetblue + bgColor: black + sectionColor: green + keyColor: dodgerblue + numKeyColor: fuchsia + frame: + title: + fgColor: aqua + bgColor: black + highlightColor: fuchsia + counterColor: papayawhip + filterColor: seagreen + border: + fgColor: dodgerblue + focusColor: lightskyblue + menu: + fgColor: white + keyColor: dodgerblue + numKeyColor: fuchsia + crumbs: + fgColor: black + bgColor: aqua + activeColor: orange + status: + newColor: lightskyblue + modifyColor: greenyellow + addColor: dodgerblue + pendingColor: darkorange + errorColor: orangered + highlightColor: aqua + killColor: mediumpurple + completedColor: lightslategray + info: + sectionColor: white + fgColor: orange + views: + table: + fgColor: aqua + bgColor: black + cursorFgColor: black + cursorBgColor: aqua + markColor: palegreen + header: + fgColor: white + bgColor: black + sorterColor: aqua + xray: + fgColor: aqua + bgColor: black + cursorColor: dodgerblue + cursorTextColor: black + graphicColor: cadetblue + charts: + bgColor: black + dialBgColor: black + chartBgColor: black + defaultDialColors: + - palegreen + - orangered + defaultChartColors: + - palegreen + - orangered + resourceColors: + cpu: + - dodgerblue + - darkslateblue + mem: + - yellow + - goldenrod + yaml: + keyColor: steelblue + valueColor: papayawhip + colonColor: white + picker: + mainColor: white + focusColor: aqua + shortcutColor: aqua + logs: + fgColor: lightskyblue + bgColor: black + indicator: + fgColor: dodgerblue + bgColor: black + toggleOnColor: limegreen + toggleOffColor: gray + dialog: + fgColor: cadetblue + bgColor: black + buttonFgColor: black + buttonBgColor: darkslateblue + buttonFocusFgColor: black + buttonFocusBgColor: dodgerblue + labelFgColor: white + fieldFgColor: white \ No newline at end of file diff --git a/internal/config/testdata/alias.yml b/internal/config/testdata/alias.yml deleted file mode 100644 index 10835dee0e..0000000000 --- a/internal/config/testdata/alias.yml +++ /dev/null @@ -1,3 +0,0 @@ -alias: - dp: "apps.v1.deployments" - pe: ".v1.pods" diff --git a/internal/config/testdata/aliases/aliases.yaml b/internal/config/testdata/aliases/aliases.yaml new file mode 100644 index 0000000000..81c781f970 --- /dev/null +++ b/internal/config/testdata/aliases/aliases.yaml @@ -0,0 +1,9 @@ +aliases: + dp: apps/v1/deployments + sec: v1/secrets + jo: batch/v1/jobs + cr: rbac.authorization.k8s.io/v1/clusterroles + crb: rbac.authorization.k8s.io/v1/clusterrolebindings + ro: rbac.authorization.k8s.io/v1/roles + rb: rbac.authorization.k8s.io/v1/rolebindings + np: networking.k8s.io/v1/networkpolicies diff --git a/internal/config/testdata/aliases/plain.yaml b/internal/config/testdata/aliases/plain.yaml new file mode 100644 index 0000000000..4291a3c7b6 --- /dev/null +++ b/internal/config/testdata/aliases/plain.yaml @@ -0,0 +1,3 @@ +aliases: + dp: "apps/v1/deployments" + pe: "v1/pods" diff --git a/internal/config/testdata/b_containers.yml b/internal/config/testdata/benchmarks/b_containers.yaml similarity index 100% rename from internal/config/testdata/b_containers.yml rename to internal/config/testdata/benchmarks/b_containers.yaml diff --git a/internal/config/testdata/b_containers_1.yml b/internal/config/testdata/benchmarks/b_containers_1.yaml similarity index 100% rename from internal/config/testdata/b_containers_1.yml rename to internal/config/testdata/benchmarks/b_containers_1.yaml diff --git a/internal/config/testdata/b_good.yml b/internal/config/testdata/benchmarks/b_good.yaml similarity index 100% rename from internal/config/testdata/b_good.yml rename to internal/config/testdata/benchmarks/b_good.yaml diff --git a/internal/config/testdata/b_toast.yml b/internal/config/testdata/benchmarks/b_toast.yaml similarity index 100% rename from internal/config/testdata/b_toast.yml rename to internal/config/testdata/benchmarks/b_toast.yaml diff --git a/internal/config/testdata/bench-fred.yml b/internal/config/testdata/benchmarks/bench-fred.yaml similarity index 100% rename from internal/config/testdata/bench-fred.yml rename to internal/config/testdata/benchmarks/bench-fred.yaml diff --git a/internal/config/testdata/configs/default.yaml b/internal/config/testdata/configs/default.yaml new file mode 100644 index 0000000000..3ea67cbfbb --- /dev/null +++ b/internal/config/testdata/configs/default.yaml @@ -0,0 +1,42 @@ +k9s: + liveViewAutoRefresh: false + screenDumpDir: /tmp/k9s-test/screen-dumps + refreshRate: 2 + maxConnRetry: 5 + readOnly: false + noExitOnCtrlC: false + ui: + enableMouse: false + headless: false + logoless: false + crumbsless: false + reactive: false + noIcons: false + defaultsToFullScreen: false + skipLatestRevCheck: false + disablePodCounting: false + shellPod: + image: busybox:1.35.0 + namespace: default + limits: + cpu: 100m + memory: 100Mi + imageScans: + enable: false + exclusions: + namespaces: [] + labels: {} + logger: + tail: 100 + buffer: 5000 + sinceSeconds: -1 + textWrap: false + showTime: false + showJSON: false + thresholds: + cpu: + critical: 90 + warn: 70 + memory: + critical: 90 + warn: 70 diff --git a/internal/config/testdata/configs/expected.yaml b/internal/config/testdata/configs/expected.yaml new file mode 100644 index 0000000000..4708f656b9 --- /dev/null +++ b/internal/config/testdata/configs/expected.yaml @@ -0,0 +1,42 @@ +k9s: + liveViewAutoRefresh: true + screenDumpDir: /tmp/k9s-test/screen-dumps + refreshRate: 100 + maxConnRetry: 5 + readOnly: true + noExitOnCtrlC: false + ui: + enableMouse: false + headless: false + logoless: false + crumbsless: false + reactive: false + noIcons: false + defaultsToFullScreen: false + skipLatestRevCheck: false + disablePodCounting: false + shellPod: + image: busybox:1.35.0 + namespace: default + limits: + cpu: 100m + memory: 100Mi + imageScans: + enable: false + exclusions: + namespaces: [] + labels: {} + logger: + tail: 500 + buffer: 800 + sinceSeconds: -1 + textWrap: false + showTime: false + showJSON: false + thresholds: + cpu: + critical: 90 + warn: 70 + memory: + critical: 90 + warn: 70 diff --git a/internal/config/testdata/configs/k9s.yaml b/internal/config/testdata/configs/k9s.yaml new file mode 100644 index 0000000000..b9837c7290 --- /dev/null +++ b/internal/config/testdata/configs/k9s.yaml @@ -0,0 +1,42 @@ +k9s: + liveViewAutoRefresh: true + screenDumpDir: /tmp/k9s-test/screen-dumps + refreshRate: 2 + maxConnRetry: 5 + readOnly: false + noExitOnCtrlC: false + ui: + enableMouse: false + headless: false + logoless: false + crumbsless: false + reactive: false + noIcons: false + defaultsToFullScreen: false + skipLatestRevCheck: false + disablePodCounting: false + shellPod: + image: busybox:1.35.0 + namespace: default + limits: + cpu: 100m + memory: 100Mi + imageScans: + enable: false + exclusions: + namespaces: [] + labels: {} + logger: + tail: 200 + buffer: 2000 + sinceSeconds: -1 + textWrap: false + showTime: false + showJSON: false + thresholds: + cpu: + critical: 90 + warn: 70 + memory: + critical: 90 + warn: 70 diff --git a/internal/config/testdata/configs/k9s_toast.yaml b/internal/config/testdata/configs/k9s_toast.yaml new file mode 100644 index 0000000000..668326ef9b --- /dev/null +++ b/internal/config/testdata/configs/k9s_toast.yaml @@ -0,0 +1,39 @@ +k9s: + liveViewAutoRefresh: true + screenDumpDir: /tmp/screen-dumps + refreshRate: 2 + maxConnRetry: 5 + readOnly: false + noExitOnCtrlC: false + ui: + enableMouse: false + headless: false + logoless: false + crumbsless: false + noIcons: false + skipLatestRevCheck: yes + disablePodCounts: false + shellPods: + image: busybox:1.35.0 + namespace: default + limits: + cpu: 100m + memory: 100Mi + imageScans: + enable: false + exclusions: + namespaces: [] + labels: {} + logger: + tail: 200 + buffer: 2000 + sinceSeconds: -1 + textWrap: false + showTime: false + thresholds: + cpu: + critical: 90 + warn: 70 + memory: + critical: 90 + warn: 70 diff --git a/internal/config/testdata/hot_key.yml b/internal/config/testdata/hotkeys/hotkeys.yaml similarity index 72% rename from internal/config/testdata/hot_key.yml rename to internal/config/testdata/hotkeys/hotkeys.yaml index 81f16319e4..55fadf8491 100644 --- a/internal/config/testdata/hot_key.yml +++ b/internal/config/testdata/hotkeys/hotkeys.yaml @@ -1,5 +1,6 @@ -hotKey: +hotKeys: pods: shortCut: shift-0 description: Launch pod view command: pods + keepHistory: true diff --git a/internal/config/testdata/k8s.yml b/internal/config/testdata/k8s.yaml similarity index 100% rename from internal/config/testdata/k8s.yml rename to internal/config/testdata/k8s.yaml diff --git a/internal/config/testdata/k9s.yml b/internal/config/testdata/k9s.yml deleted file mode 100644 index e98a2180cf..0000000000 --- a/internal/config/testdata/k9s.yml +++ /dev/null @@ -1,34 +0,0 @@ -k9s: - liveViewAutoRefresh: true - refreshRate: 2 - readOnly: false - logger: - tail: 200 - buffer: 2000 - currentContext: minikube - currentCluster: minikube - clusters: - minikube: - namespace: - active: kube-system - favorites: - - default - - kube-public - - istio-system - - all - - kube-system - view: - active: ctx - fred: - namespace: - active: default - favorites: - - default - - kube-public - - istio-system - - all - - kube-system - view: - active: po - screenDumpDir: /tmp - disablePodCounting: false diff --git a/internal/config/testdata/k9s1.yml b/internal/config/testdata/k9s1.yml deleted file mode 100644 index 99bb975f65..0000000000 --- a/internal/config/testdata/k9s1.yml +++ /dev/null @@ -1,8 +0,0 @@ -k9s: - refreshRate: 10 - namespace: - active: fred - favorites: - - blee - - duh - - crap \ No newline at end of file diff --git a/internal/config/testdata/k9s_old.yml b/internal/config/testdata/k9s_old.yml deleted file mode 100644 index dade14d9a3..0000000000 --- a/internal/config/testdata/k9s_old.yml +++ /dev/null @@ -1,13 +0,0 @@ -k9s: - refreshRate: 2 - logBufferSize: 200 - namespace: - active: kube-system - favorites: - - default - - kube-public - - istio-system - - all - - kube-system - view: - active: ctx \ No newline at end of file diff --git a/internal/config/testdata/k9s_readonly.yml b/internal/config/testdata/k9s_readonly.yml deleted file mode 100644 index e8c5c1928b..0000000000 --- a/internal/config/testdata/k9s_readonly.yml +++ /dev/null @@ -1,31 +0,0 @@ -k9s: - refreshRate: 2 - readOnly: true - logger: - tail: 200 - buffer: 2000 - currentContext: minikube - currentCluster: minikube - clusters: - minikube: - namespace: - active: kube-system - favorites: - - default - - kube-public - - istio-system - - all - - kube-system - view: - active: ctx - fred: - namespace: - active: default - favorites: - - default - - kube-public - - istio-system - - all - - kube-system - view: - active: po diff --git a/internal/config/testdata/k9s_toast.yml b/internal/config/testdata/k9s_toast.yml deleted file mode 100644 index 189706a655..0000000000 --- a/internal/config/testdata/k9s_toast.yml +++ /dev/null @@ -1,28 +0,0 @@ -k9s: - refreshRate: 2 - logBufferSize: 200 - currentContext: minikube - currentCluster: minikube - clusters: - minikube: - namespace: - active: kube-system - favorites: - - default - - kube-public - - istio-system - - all - - kube-system - view: - active: ctx - fred: - namespace: - active: default - favorites: - - default - - kube-public - - istio-system - - all - - kube-system - view: - active: po \ No newline at end of file diff --git a/internal/config/testdata/kubeconfig-test.yml b/internal/config/testdata/kubes/test.yaml similarity index 56% rename from internal/config/testdata/kubeconfig-test.yml rename to internal/config/testdata/kubes/test.yaml index a3c72a7af9..759598b048 100644 --- a/internal/config/testdata/kubeconfig-test.yml +++ b/internal/config/testdata/kubes/test.yaml @@ -4,19 +4,28 @@ clusters: - cluster: certificate-authority: /Users/test/ca.crt server: https://1.2.3.4:8443 - name: testCluster + name: cl-1 + - cluster: + certificate-authority: /Users/test/ca.crt + server: https://5.6.7.8:8443 + name: cl-2 contexts: - context: - cluster: cluster1 + cluster: cl-1 user: user1 - namespace: ns1 - name: test1 + namespace: ns-1 + name: ct-1-1 + - context: + cluster: cl-1 + user: user2 + namespace: ns-2 + name: ct-1-2 - context: - cluster: cluster2 + cluster: cl-2 user: user2 - namespace: ns2 - name: test2 -current-context: test1 + namespace: ns-2 + name: ct-2-1 +current-context: ct-1-1 preferences: {} users: - name: user1 diff --git a/internal/config/testdata/plugin.yml b/internal/config/testdata/plugins.yaml similarity index 95% rename from internal/config/testdata/plugin.yml rename to internal/config/testdata/plugins.yaml index 0563f6f827..cfa4748967 100644 --- a/internal/config/testdata/plugin.yml +++ b/internal/config/testdata/plugins.yaml @@ -1,4 +1,4 @@ -plugin: +plugins: blah: shortCut: shift-s confirm: true diff --git a/internal/config/testdata/plugins/test1.yml b/internal/config/testdata/plugins/test1.yaml similarity index 100% rename from internal/config/testdata/plugins/test1.yml rename to internal/config/testdata/plugins/test1.yaml diff --git a/internal/config/testdata/plugins/test2.yml b/internal/config/testdata/plugins/test2.yaml similarity index 100% rename from internal/config/testdata/plugins/test2.yml rename to internal/config/testdata/plugins/test2.yaml diff --git a/internal/config/testdata/black_and_wtf.yml b/internal/config/testdata/skins/black-and-wtf.yaml similarity index 82% rename from internal/config/testdata/black_and_wtf.yml rename to internal/config/testdata/skins/black-and-wtf.yaml index 5cef5c954f..2ad58452a0 100644 --- a/internal/config/testdata/black_and_wtf.yml +++ b/internal/config/testdata/skins/black-and-wtf.yaml @@ -31,11 +31,12 @@ k9s: highlightColor: navajowhite counterColor: navajowhite filterColor: slategray - table: - fgColor: white - bgColor: black - cursorColor: white - header: - fgColor: darkgray + views: + table: + fgColor: white bgColor: black - sorterColor: white + cursorColor: white + header: + fgColor: darkgray + bgColor: black + sorterColor: white diff --git a/internal/config/testdata/skin_boarked.yml b/internal/config/testdata/skins/boarked.yaml similarity index 100% rename from internal/config/testdata/skin_boarked.yml rename to internal/config/testdata/skins/boarked.yaml diff --git a/internal/config/testdata/skins/empty.yaml b/internal/config/testdata/skins/empty.yaml new file mode 100644 index 0000000000..27de2f65c0 --- /dev/null +++ b/internal/config/testdata/skins/empty.yaml @@ -0,0 +1,2 @@ +k9s: + body: \ No newline at end of file diff --git a/internal/config/testdata/view_settings.yml b/internal/config/testdata/view_settings.yml deleted file mode 100644 index 3ea3050c6e..0000000000 --- a/internal/config/testdata/view_settings.yml +++ /dev/null @@ -1,8 +0,0 @@ -k9s: - views: - v1/pods: - columns: - - NAMESPACE - - NAME - - AGE - - IP diff --git a/internal/config/testdata/views/views.yaml b/internal/config/testdata/views/views.yaml new file mode 100644 index 0000000000..b6debac30a --- /dev/null +++ b/internal/config/testdata/views/views.yaml @@ -0,0 +1,7 @@ +views: + v1/pods: + columns: + - NAMESPACE + - NAME + - AGE + - IP diff --git a/internal/config/threshold.go b/internal/config/threshold.go index 1db1ccfa97..de01250a67 100644 --- a/internal/config/threshold.go +++ b/internal/config/threshold.go @@ -1,8 +1,7 @@ -package config +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s -import ( - "github.com/derailed/k9s/internal/client" -) +package config const ( // SeverityLow tracks low severity. @@ -62,7 +61,7 @@ func NewThreshold() Threshold { } // Validate a namespace is setup correctly. -func (t Threshold) Validate(c client.Connection, ks KubeSettings) { +func (t Threshold) Validate() Threshold { for _, k := range []string{"cpu", "memory"} { v, ok := t[k] if !ok { @@ -71,6 +70,8 @@ func (t Threshold) Validate(c client.Connection, ks KubeSettings) { v.Validate() } } + + return t } // LevelFor returns a defcon level for the current state. diff --git a/internal/config/threshold_test.go b/internal/config/threshold_test.go index 1c1be9370c..11cabae3bf 100644 --- a/internal/config/threshold_test.go +++ b/internal/config/threshold_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config_test import ( diff --git a/internal/config/types.go b/internal/config/types.go new file mode 100644 index 0000000000..6938e55585 --- /dev/null +++ b/internal/config/types.go @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package config + +const ( + defaultRefreshRate = 2 + defaultMaxConnRetry = 5 +) + +// UI tracks ui specific configs. +type UI struct { + // EnableMouse toggles mouse support. + EnableMouse bool `json:"enableMouse" yaml:"enableMouse"` + + // Headless toggles top header display. + Headless bool `json:"headless" yaml:"headless"` + + // LogoLess toggles k9s logo. + Logoless bool `json:"logoless" yaml:"logoless"` + + // Crumbsless toggles nav crumb display. + Crumbsless bool `json:"crumbsless" yaml:"crumbsless"` + + // Reactive toggles reactive ui changes. + Reactive bool `json:"reactive" yaml:"reactive"` + + // NoIcons toggles icons display. + NoIcons bool `json:"noIcons" yaml:"noIcons"` + + // Skin reference the general k9s skin name. + // Can be overridden per context. + Skin string `json:"skin" yaml:"skin,omitempty"` + + // DefaultsToFullScreen toggles fullscreen on views like logs, yaml, details. + DefaultsToFullScreen bool `json:"defaultsToFullScreen" yaml:"defaultsToFullScreen"` +} diff --git a/internal/config/views.go b/internal/config/views.go index 4c10a9e9d3..cafb9053da 100644 --- a/internal/config/views.go +++ b/internal/config/views.go @@ -1,18 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config import ( + "cmp" + "errors" + "fmt" + "io/fs" "os" - "path/filepath" + "slices" + "strings" + + "github.com/derailed/k9s/internal/config/data" + "github.com/derailed/k9s/internal/config/json" "gopkg.in/yaml.v2" ) -// K9sViewConfigFile represents the location for the views configuration. -var K9sViewConfigFile = filepath.Join(K9sHome(), "views.yml") - // ViewConfigListener represents a view config listener. type ViewConfigListener interface { - // ConfigChanged notifies listener the view configuration changed. + // ViewSettingsChanged notifies listener the view configuration changed. ViewSettingsChanged(ViewSetting) } @@ -22,51 +30,74 @@ type ViewSetting struct { SortColumn string `yaml:"sortColumn"` } -// ViewSettings represent a collection of view configurations. -type ViewSettings struct { - Views map[string]ViewSetting `yaml:"views"` +func (v *ViewSetting) HasCols() bool { + return len(v.Columns) > 0 +} + +func (v *ViewSetting) IsBlank() bool { + return v == nil || len(v.Columns) == 0 +} + +func (v *ViewSetting) SortCol() (string, bool, error) { + if v == nil || v.SortColumn == "" { + return "", false, fmt.Errorf("no sort column specified") + } + tt := strings.Split(v.SortColumn, ":") + if len(tt) < 2 { + return "", false, fmt.Errorf("invalid sort column spec: %q. must be col-name:asc|desc", v.SortColumn) + } + + return tt[0], tt[1] == "desc", nil } -// NewViewSettings returns a new configuration. -func NewViewSettings() ViewSettings { - return ViewSettings{ - Views: make(map[string]ViewSetting), +func (v *ViewSetting) Equals(vs *ViewSetting) bool { + if v == nil || vs == nil { + return v == nil && vs == nil + } + if c := slices.Compare(v.Columns, vs.Columns); c != 0 { + return false } + return cmp.Compare(v.SortColumn, vs.SortColumn) == 0 } // CustomView represents a collection of view customization. type CustomView struct { - K9s ViewSettings `yaml:"k9s"` + Views map[string]ViewSetting `yaml:"views"` listeners map[string]ViewConfigListener } // NewCustomView returns a views configuration. func NewCustomView() *CustomView { return &CustomView{ - K9s: NewViewSettings(), + Views: make(map[string]ViewSetting), listeners: make(map[string]ViewConfigListener), } } // Reset clears out configurations. func (v *CustomView) Reset() { - for k := range v.K9s.Views { - delete(v.K9s.Views, k) + for k := range v.Views { + delete(v.Views, k) } } // Load loads view configurations. func (v *CustomView) Load(path string) error { - raw, err := os.ReadFile(path) + if _, err := os.Stat(path); errors.Is(err, fs.ErrNotExist) { + return nil + } + bb, err := os.ReadFile(path) if err != nil { return err } - + if err := data.JSONValidator.Validate(json.ViewsSchema, bb); err != nil { + return fmt.Errorf("validation failed for %q: %w", path, err) + } var in CustomView - if err := yaml.Unmarshal(raw, &in); err != nil { + if err := yaml.Unmarshal(bb, &in); err != nil { return err } - v.K9s = in.K9s + v.Views = in.Views v.fireConfigChanged() return nil @@ -85,8 +116,10 @@ func (v *CustomView) RemoveListener(gvr string) { func (v *CustomView) fireConfigChanged() { for gvr, list := range v.listeners { - if v, ok := v.K9s.Views[gvr]; ok { + if v, ok := v.Views[gvr]; ok { list.ViewSettingsChanged(v) + } else { + list.ViewSettingsChanged(ViewSetting{}) } } } diff --git a/internal/config/views_test.go b/internal/config/views_test.go index 0005cfd20e..0764d2435e 100644 --- a/internal/config/views_test.go +++ b/internal/config/views_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package config_test import ( @@ -10,7 +13,28 @@ import ( func TestViewSettingsLoad(t *testing.T) { cfg := config.NewCustomView() - assert.Nil(t, cfg.Load("testdata/view_settings.yml")) - assert.Equal(t, 1, len(cfg.K9s.Views)) - assert.Equal(t, 4, len(cfg.K9s.Views["v1/pods"].Columns)) + assert.Nil(t, cfg.Load("testdata/views/views.yaml")) + assert.Equal(t, 1, len(cfg.Views)) + assert.Equal(t, 4, len(cfg.Views["v1/pods"].Columns)) +} + +func TestViewSetting_Equals(t *testing.T) { + tests := []struct { + v1, v2 *config.ViewSetting + equals bool + }{ + {nil, nil, true}, + {&config.ViewSetting{}, nil, false}, + {nil, &config.ViewSetting{}, false}, + {&config.ViewSetting{}, &config.ViewSetting{}, true}, + {&config.ViewSetting{Columns: []string{"A"}}, &config.ViewSetting{}, false}, + {&config.ViewSetting{Columns: []string{"A"}}, &config.ViewSetting{Columns: []string{"A"}}, true}, + {&config.ViewSetting{Columns: []string{"A"}}, &config.ViewSetting{Columns: []string{"B"}}, false}, + {&config.ViewSetting{SortColumn: "A"}, &config.ViewSetting{SortColumn: "B"}, false}, + {&config.ViewSetting{SortColumn: "A"}, &config.ViewSetting{SortColumn: "A"}, true}, + } + + for _, tt := range tests { + assert.Equalf(t, tt.equals, tt.v1.Equals(tt.v2), "%#v and %#v", tt.v1, tt.v2) + } } diff --git a/internal/dao/alias.go b/internal/dao/alias.go index 4c073527bc..bf4bf308d5 100644 --- a/internal/dao/alias.go +++ b/internal/dao/alias.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -11,6 +14,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/view/cmd" "k8s.io/apimachinery/pkg/runtime" ) @@ -19,21 +23,27 @@ var _ Accessor = (*Alias)(nil) // Alias tracks standard and custom command aliases. type Alias struct { NonResource + *config.Aliases } // NewAlias returns a new set of aliases. func NewAlias(f Factory) *Alias { - a := Alias{Aliases: config.NewAliases()} + a := Alias{ + Aliases: config.NewAliases(), + } a.Init(f, client.NewGVR("aliases")) return &a } +func (a *Alias) AliasesFor(s string) []string { + return a.Aliases.AliasesFor(s) +} + // Check verifies an alias is defined for this command. -func (a *Alias) Check(cmd string) bool { - _, ok := a.Aliases.Get(cmd) - return ok +func (a *Alias) Check(cmd string) (string, bool) { + return a.Aliases.Get(cmd) } // List returns a collection of aliases. @@ -46,19 +56,30 @@ func (a *Alias) List(ctx context.Context, _ string) ([]runtime.Object, error) { oo := make([]runtime.Object, 0, len(m)) for gvr, aliases := range m { sort.StringSlice(aliases).Sort() - oo = append(oo, render.AliasRes{GVR: gvr, Aliases: aliases}) + oo = append(oo, render.AliasRes{ + GVR: gvr, + Aliases: aliases, + }) } return oo, nil } // AsGVR returns a matching gvr if it exists. -func (a *Alias) AsGVR(cmd string) (client.GVR, bool) { - gvr, ok := a.Aliases.Get(cmd) - if ok { - return client.NewGVR(gvr), true +func (a *Alias) AsGVR(c string) (client.GVR, string, bool) { + exp, ok := a.Aliases.Get(c) + if !ok { + return client.NoGVR, "", ok + } + p := cmd.NewInterpreter(exp) + if strings.Contains(p.Cmd(), "/") { + return client.NewGVR(p.Cmd()), "", true } - return client.GVR{}, false + if gvr, ok := a.Aliases.Get(p.Cmd()); ok { + return client.NewGVR(gvr), exp, true + } + + return client.NoGVR, "", false } // Get fetch a resource. @@ -67,15 +88,15 @@ func (a *Alias) Get(_ context.Context, _ string) (runtime.Object, error) { } // Ensure makes sure alias are loaded. -func (a *Alias) Ensure() (config.Alias, error) { +func (a *Alias) Ensure(path string) (config.Alias, error) { if err := MetaAccess.LoadResources(a.Factory); err != nil { return config.Alias{}, err } - return a.Alias, a.load() + return a.Alias, a.load(path) } -func (a *Alias) load() error { - if err := a.Load(); err != nil { +func (a *Alias) load(path string) error { + if err := a.Load(path); err != nil { return err } diff --git a/internal/dao/alias_test.go b/internal/dao/alias_test.go index 337c78a76a..315c06f7ac 100644 --- a/internal/dao/alias_test.go +++ b/internal/dao/alias_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao_test import ( @@ -9,13 +12,51 @@ import ( "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/render" - "github.com/derailed/k9s/internal/watch" "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/informers" ) +func TestAsGVR(t *testing.T) { + a := dao.NewAlias(makeFactory()) + a.Aliases.Define("v1/pods", "po", "pod", "pods") + a.Aliases.Define("workloads", "workloads", "workload", "wkl") + + uu := map[string]struct { + cmd string + ok bool + gvr client.GVR + }{ + "ok": { + cmd: "pods", + ok: true, + gvr: client.NewGVR("v1/pods"), + }, + "ok-short": { + cmd: "po", + ok: true, + gvr: client.NewGVR("v1/pods"), + }, + "missing": { + cmd: "zorg", + }, + "alias": { + cmd: "wkl", + ok: true, + gvr: client.NewGVR("workloads"), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + gvr, _, ok := a.AsGVR(u.cmd) + assert.Equal(t, u.ok, ok) + if u.ok { + assert.Equal(t, u.gvr, gvr) + } + }) + } +} + func TestAliasList(t *testing.T) { a := dao.Alias{} a.Init(makeFactory(), client.NewGVR("aliases")) @@ -43,36 +84,3 @@ func makeAliases() *dao.Alias { }, } } - -type testFactory struct{} - -var _ dao.Factory = testFactory{} - -func (f testFactory) Client() client.Connection { - return nil -} - -func (f testFactory) Get(gvr, path string, wait bool, sel labels.Selector) (runtime.Object, error) { - return nil, nil -} - -func (f testFactory) List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) { - return nil, nil -} - -func (f testFactory) ForResource(ns, gvr string) (informers.GenericInformer, error) { - return nil, nil -} - -func (f testFactory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) { - return nil, nil -} -func (f testFactory) WaitForCacheSync() {} -func (f testFactory) Forwarders() watch.Forwarders { - return nil -} -func (f testFactory) DeleteForwarder(string) {} - -func makeFactory() dao.Factory { - return testFactory{} -} diff --git a/internal/dao/benchmark.go b/internal/dao/benchmark.go index d0d1afe678..683cb2a4b2 100644 --- a/internal/dao/benchmark.go +++ b/internal/dao/benchmark.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -42,20 +45,21 @@ func (b *Benchmark) List(ctx context.Context, _ string) ([]runtime.Object, error if !ok { return nil, errors.New("no benchmark dir found in context") } - path, _ := ctx.Value(internal.KeyPath).(string) + path, ok := ctx.Value(internal.KeyPath).(string) + if !ok { + return nil, errors.New("no path specified in context") + } + pathMatch := BenchRx.ReplaceAllString(strings.Replace(path, "/", "_", 1), "_") ff, err := os.ReadDir(dir) if err != nil { return nil, err } - - fileName := BenchRx.ReplaceAllString(strings.Replace(path, "/", "_", 1), "_") oo := make([]runtime.Object, 0, len(ff)) for _, f := range ff { - if path != "" && !strings.HasPrefix(f.Name(), fileName) { + if !strings.HasPrefix(f.Name(), pathMatch) { continue } - if fi, err := f.Info(); err == nil { oo = append(oo, render.BenchInfo{File: fi, Path: filepath.Join(dir, f.Name())}) } diff --git a/internal/dao/benchmark_test.go b/internal/dao/benchmark_test.go index b3e52f2192..12fed066d8 100644 --- a/internal/dao/benchmark_test.go +++ b/internal/dao/benchmark_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao_test import ( @@ -16,6 +19,7 @@ func TestBenchmarkList(t *testing.T) { a.Init(makeFactory(), client.NewGVR("benchmarks")) ctx := context.WithValue(context.Background(), internal.KeyDir, "testdata/bench") + ctx = context.WithValue(ctx, internal.KeyPath, "") oo, err := a.List(ctx, "-") assert.Nil(t, err) diff --git a/internal/dao/cluster.go b/internal/dao/cluster.go index 38d8cb5ca2..dab4fbf158 100644 --- a/internal/dao/cluster.go +++ b/internal/dao/cluster.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -16,7 +19,7 @@ type RefScanner interface { // Init initializes the scanner Init(Factory, client.GVR) // Scan scan the resource for references. - Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error) + Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) } @@ -35,7 +38,7 @@ var ( _ RefScanner = (*DaemonSet)(nil) _ RefScanner = (*Job)(nil) _ RefScanner = (*CronJob)(nil) - _ RefScanner = (*Pod)(nil) + // _ RefScanner = (*Pod)(nil) ) func scanners() map[string]RefScanner { @@ -45,7 +48,7 @@ func scanners() map[string]RefScanner { "apps/v1/daemonsets": &DaemonSet{}, "batch/v1/jobs": &Job{}, "batch/v1/cronjobs": &CronJob{}, - "v1/pods": &Pod{}, + // "v1/pods": &Pod{}, } } @@ -55,7 +58,7 @@ func ScanForRefs(ctx context.Context, f Factory) (Refs, error) { log.Debug().Msgf("Cluster Scan %v", time.Since(t)) }(time.Now()) - gvr, ok := ctx.Value(internal.KeyGVR).(string) + gvr, ok := ctx.Value(internal.KeyGVR).(client.GVR) if !ok { return nil, errors.New("expecting context GVR") } diff --git a/internal/dao/cm.go b/internal/dao/cm.go new file mode 100644 index 0000000000..0910d70495 --- /dev/null +++ b/internal/dao/cm.go @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package dao + +var ( + _ Accessor = (*ConfigMap)(nil) +) + +// ConfigMap represents a configmap resource. +type ConfigMap struct { + Resource +} diff --git a/internal/dao/container.go b/internal/dao/container.go index 216d40ee7d..2074607f26 100644 --- a/internal/dao/container.go +++ b/internal/dao/container.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -35,7 +38,7 @@ func (c *Container) List(ctx context.Context, _ string) ([]runtime.Object, error cmx client.ContainersMetrics err error ) - if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok { + if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); ok && withMx { cmx, _ = client.DialMetrics(c.Client()).FetchContainersMetrics(ctx, fqn) } @@ -91,7 +94,7 @@ func getContainerStatus(co string, status v1.PodStatus) *v1.ContainerStatus { } func (c *Container) fetchPod(fqn string) (*v1.Pod, error) { - o, err := c.GetFactory().Get("v1/pods", fqn, true, labels.Everything()) + o, err := c.getFactory().Get("v1/pods", fqn, true, labels.Everything()) if err != nil { return nil, err } diff --git a/internal/dao/container_test.go b/internal/dao/container_test.go index 61df494829..f53c82959d 100644 --- a/internal/dao/container_test.go +++ b/internal/dao/container_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao_test import ( @@ -59,16 +62,18 @@ func (c *conn) ValidNamespaces() ([]v1.Namespace, error) { return n func (c *conn) SupportsRes(grp string, versions []string) (string, bool, error) { return "", false, nil } -func (c *conn) ServerVersion() (*version.Info, error) { return nil, nil } -func (c *conn) CurrentNamespaceName() (string, error) { return "", nil } -func (c *conn) CanI(ns, gvr string, verbs []string) (bool, error) { return true, nil } -func (c *conn) ActiveCluster() string { return "" } -func (c *conn) ActiveNamespace() string { return "" } -func (c *conn) IsActiveNamespace(string) bool { return false } +func (c *conn) ServerVersion() (*version.Info, error) { return nil, nil } +func (c *conn) CurrentNamespaceName() (string, error) { return "", nil } +func (c *conn) CanI(ns, gvr, n string, verbs []string) (bool, error) { return true, nil } +func (c *conn) ActiveContext() string { return "" } +func (c *conn) ActiveNamespace() string { return "" } +func (c *conn) IsValidNamespace(string) bool { return true } +func (c *conn) ValidNamespaceNames() (client.NamespaceNames, error) { return nil, nil } +func (c *conn) IsActiveNamespace(string) bool { return false } type podFactory struct{} -var _ dao.Factory = testFactory{} +var _ dao.Factory = &testFactory{} func (f podFactory) Client() client.Connection { return makeConn() diff --git a/internal/dao/context.go b/internal/dao/context.go index 62ac939dc3..671ed34f02 100644 --- a/internal/dao/context.go +++ b/internal/dao/context.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -20,7 +23,7 @@ type Context struct { } func (c *Context) config() *client.Config { - return c.GetFactory().Client().Config() + return c.getFactory().Client().Config() } // Get a Context. @@ -57,5 +60,5 @@ func (c *Context) MustCurrentContextName() string { // Switch to another context. func (c *Context) Switch(ctx string) error { - return c.GetFactory().Client().SwitchContext(ctx) + return c.getFactory().Client().SwitchContext(ctx) } diff --git a/internal/dao/crd.go b/internal/dao/crd.go index d05d5f9a05..5f8455a27c 100644 --- a/internal/dao/crd.go +++ b/internal/dao/crd.go @@ -1,13 +1,7 @@ -package dao - -import ( - "context" +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s - "github.com/derailed/k9s/internal" - v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" -) +package dao var ( _ Accessor = (*CustomResourceDefinition)(nil) @@ -18,28 +12,3 @@ var ( type CustomResourceDefinition struct { Resource } - -// IsHappy check for happy deployments. -func (c *CustomResourceDefinition) IsHappy(crd v1.CustomResourceDefinition) bool { - versions := make([]string, 0, 3) - for _, v := range crd.Spec.Versions { - if v.Served && !v.Deprecated { - versions = append(versions, v.Name) - break - } - } - - return len(versions) > 0 -} - -// List returns a collection of nodes. -func (c *CustomResourceDefinition) List(ctx context.Context, _ string) ([]runtime.Object, error) { - strLabel, ok := ctx.Value(internal.KeyLabels).(string) - labelSel := labels.Everything() - if sel, e := labels.ConvertSelectorToLabelsMap(strLabel); ok && e == nil { - labelSel = sel.AsSelector() - } - - const gvr = "apiextensions.k8s.io/v1/customresourcedefinitions" - return c.GetFactory().List(gvr, "-", false, labelSel) -} diff --git a/internal/dao/cronjob.go b/internal/dao/cronjob.go index 9c5d5011f9..3153349571 100644 --- a/internal/dao/cronjob.go +++ b/internal/dao/cronjob.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -6,6 +9,7 @@ import ( "fmt" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" "github.com/rs/zerolog/log" batchv1 "k8s.io/api/batch/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -21,8 +25,9 @@ const ( ) var ( - _ Accessor = (*CronJob)(nil) - _ Runnable = (*CronJob)(nil) + _ Accessor = (*CronJob)(nil) + _ Runnable = (*CronJob)(nil) + _ ImageLister = (*CronJob)(nil) ) // CronJob represents a cronjob K8s resource. @@ -30,10 +35,20 @@ type CronJob struct { Generic } +// ListImages lists container images. +func (c *CronJob) ListImages(ctx context.Context, fqn string) ([]string, error) { + cj, err := c.GetInstance(fqn) + if err != nil { + return nil, err + } + + return render.ExtractImages(&cj.Spec.JobTemplate.Spec.Template.Spec), nil +} + // Run a CronJob. func (c *CronJob) Run(path string) error { - ns, _ := client.Namespaced(path) - auth, err := c.Client().CanI(ns, jobGVR, []string{client.GetVerb, client.CreateVerb}) + ns, n := client.Namespaced(path) + auth, err := c.Client().CanI(ns, jobGVR, n, []string{client.GetVerb, client.CreateVerb}) if err != nil { return err } @@ -41,7 +56,7 @@ func (c *CronJob) Run(path string) error { return fmt.Errorf("user is not authorized to run jobs") } - o, err := c.GetFactory().Get(c.GVR(), path, true, labels.Everything()) + o, err := c.getFactory().Get(c.GVR(), path, true, labels.Everything()) if err != nil { return err } @@ -57,15 +72,16 @@ func (c *CronJob) Run(path string) error { true := true job := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ - Name: jobName + "-manual-" + rand.String(3), - Namespace: ns, - Labels: cj.Spec.JobTemplate.Labels, + Name: jobName + "-manual-" + rand.String(3), + Namespace: ns, + Labels: cj.Spec.JobTemplate.Labels, Annotations: cj.Spec.JobTemplate.Annotations, OwnerReferences: []metav1.OwnerReference{ { APIVersion: c.gvr.GV().String(), Kind: "CronJob", BlockOwnerDeletion: &true, + Controller: &true, Name: cj.Name, UID: cj.UID, }, @@ -87,7 +103,7 @@ func (c *CronJob) Run(path string) error { // ScanSA scans for serviceaccount refs. func (c *CronJob) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) { ns, n := client.Namespaced(fqn) - oo, err := c.GetFactory().List(c.GVR(), ns, wait, labels.Everything()) + oo, err := c.getFactory().List(c.GVR(), ns, wait, labels.Everything()) if err != nil { return nil, err } @@ -110,10 +126,26 @@ func (c *CronJob) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, erro return refs, nil } +// GetInstance fetch a matching cronjob. +func (c *CronJob) GetInstance(fqn string) (*batchv1.CronJob, error) { + o, err := c.getFactory().Get(c.GVR(), fqn, true, labels.Everything()) + if err != nil { + return nil, err + } + + var cj batchv1.CronJob + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &cj) + if err != nil { + return nil, errors.New("expecting cronjob resource") + } + + return &cj, nil +} + // ToggleSuspend toggles suspend/resume on a CronJob. func (c *CronJob) ToggleSuspend(ctx context.Context, path string) error { ns, n := client.Namespaced(path) - auth, err := c.Client().CanI(ns, c.GVR(), []string{client.GetVerb, client.UpdateVerb}) + auth, err := c.Client().CanI(ns, c.GVR(), n, []string{client.GetVerb, client.UpdateVerb}) if err != nil { return err } @@ -142,9 +174,9 @@ func (c *CronJob) ToggleSuspend(ctx context.Context, path string) error { } // Scan scans for cluster resource refs. -func (c *CronJob) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error) { +func (c *CronJob) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) { ns, n := client.Namespaced(fqn) - oo, err := c.GetFactory().List(c.GVR(), ns, wait, labels.Everything()) + oo, err := c.getFactory().List(c.GVR(), ns, wait, labels.Everything()) if err != nil { return nil, err } @@ -157,7 +189,7 @@ func (c *CronJob) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, e return nil, errors.New("expecting CronJob resource") } switch gvr { - case "v1/configmaps": + case CmGVR: if !hasConfigMap(&cj.Spec.JobTemplate.Spec.Template.Spec, n) { continue } @@ -165,7 +197,7 @@ func (c *CronJob) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, e GVR: c.GVR(), FQN: client.FQN(cj.Namespace, cj.Name), }) - case "v1/secrets": + case SecGVR: found, err := hasSecret(c.Factory, &cj.Spec.JobTemplate.Spec.Template.Spec, cj.Namespace, n, wait) if err != nil { log.Warn().Err(err).Msgf("locate secret %q", fqn) @@ -178,7 +210,7 @@ func (c *CronJob) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, e GVR: c.GVR(), FQN: client.FQN(cj.Namespace, cj.Name), }) - case "scheduling.k8s.io/v1/priorityclasses": + case PcGVR: if !hasPC(&cj.Spec.JobTemplate.Spec.Template.Spec, n) { continue } diff --git a/internal/dao/cruiser.go b/internal/dao/cruiser.go index 4709827eaf..25a194eeb4 100644 --- a/internal/dao/cruiser.go +++ b/internal/dao/cruiser.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( diff --git a/internal/dao/cruiser_test.go b/internal/dao/cruiser_test.go index da8769e7f4..6e88cb8649 100644 --- a/internal/dao/cruiser_test.go +++ b/internal/dao/cruiser_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( diff --git a/internal/dao/describe.go b/internal/dao/describe.go index 6d40d482ae..d990dfd405 100644 --- a/internal/dao/describe.go +++ b/internal/dao/describe.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -23,7 +26,7 @@ func Describe(c client.Connection, gvr client.GVR, path string) (string, error) ns, n := client.Namespaced(path) if client.IsClusterScoped(ns) { - ns = client.AllNamespaces + ns = client.BlankNamespace } mapping, err := mapper.ResourceFor(gvr.AsResourceName(), gvk.Kind) if err != nil { diff --git a/internal/dao/dir.go b/internal/dao/dir.go index b5a3a45627..95239cc070 100644 --- a/internal/dao/dir.go +++ b/internal/dao/dir.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( diff --git a/internal/dao/dir_test.go b/internal/dao/dir_test.go index cd604484e5..c33e5d785a 100644 --- a/internal/dao/dir_test.go +++ b/internal/dao/dir_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao_test import ( diff --git a/internal/dao/dp.go b/internal/dao/dp.go index 8e21d0c46f..5df3432113 100644 --- a/internal/dao/dp.go +++ b/internal/dao/dp.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -6,6 +9,7 @@ import ( "fmt" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" "github.com/rs/zerolog/log" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" @@ -27,6 +31,7 @@ var ( _ Scalable = (*Deployment)(nil) _ Controller = (*Deployment)(nil) _ ContainsPodSpec = (*Deployment)(nil) + _ ImageLister = (*Deployment)(nil) ) // Deployment represents a deployment K8s resource. @@ -34,15 +39,20 @@ type Deployment struct { Resource } -// IsHappy check for happy deployments. -func (d *Deployment) IsHappy(dp appsv1.Deployment) bool { - return dp.Status.Replicas == dp.Status.AvailableReplicas +// ListImages lists container images. +func (d *Deployment) ListImages(ctx context.Context, fqn string) ([]string, error) { + dp, err := d.GetInstance(fqn) + if err != nil { + return nil, err + } + + return render.ExtractImages(&dp.Spec.Template.Spec), nil } // Scale a Deployment. func (d *Deployment) Scale(ctx context.Context, path string, replicas int32) error { ns, n := client.Namespaced(path) - auth, err := d.Client().CanI(ns, "apps/v1/deployments:scale", []string{client.GetVerb, client.UpdateVerb}) + auth, err := d.Client().CanI(ns, "apps/v1/deployments:scale", n, []string{client.GetVerb, client.UpdateVerb}) if err != nil { return err } @@ -66,7 +76,7 @@ func (d *Deployment) Scale(ctx context.Context, path string, replicas int32) err // Restart a Deployment rollout. func (d *Deployment) Restart(ctx context.Context, path string) error { - o, err := d.GetFactory().Get("apps/v1/deployments", path, true, labels.Everything()) + o, err := d.getFactory().Get("apps/v1/deployments", path, true, labels.Everything()) if err != nil { return err } @@ -76,7 +86,7 @@ func (d *Deployment) Restart(ctx context.Context, path string) error { return err } - auth, err := d.Client().CanI(dp.Namespace, "apps/v1/deployments", []string{client.PatchVerb}) + auth, err := d.Client().CanI(dp.Namespace, "apps/v1/deployments", dp.Name, client.PatchAccess) if err != nil { return err } @@ -115,7 +125,7 @@ func (d *Deployment) Restart(ctx context.Context, path string) error { // TailLogs tail logs for all pods represented by this Deployment. func (d *Deployment) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) { - dp, err := d.GetInstance(d.Factory, opts.Path) + dp, err := d.GetInstance(opts.Path) if err != nil { return nil, err } @@ -128,7 +138,7 @@ func (d *Deployment) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, // Pod returns a pod victim by name. func (d *Deployment) Pod(fqn string) (string, error) { - dp, err := d.GetInstance(d.Factory, fqn) + dp, err := d.GetInstance(fqn) if err != nil { return "", err } @@ -137,8 +147,8 @@ func (d *Deployment) Pod(fqn string) (string, error) { } // GetInstance fetch a matching deployment. -func (*Deployment) GetInstance(f Factory, fqn string) (*appsv1.Deployment, error) { - o, err := f.Get("apps/v1/deployments", fqn, true, labels.Everything()) +func (d *Deployment) GetInstance(fqn string) (*appsv1.Deployment, error) { + o, err := d.Factory.Get(d.GVR(), fqn, true, labels.Everything()) if err != nil { return nil, err } @@ -155,7 +165,7 @@ func (*Deployment) GetInstance(f Factory, fqn string) (*appsv1.Deployment, error // ScanSA scans for serviceaccount refs. func (d *Deployment) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) { ns, n := client.Namespaced(fqn) - oo, err := d.GetFactory().List(d.GVR(), ns, wait, labels.Everything()) + oo, err := d.getFactory().List(d.GVR(), ns, wait, labels.Everything()) if err != nil { return nil, err } @@ -179,9 +189,9 @@ func (d *Deployment) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, e } // Scan scans for resource references. -func (d *Deployment) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error) { +func (d *Deployment) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) { ns, n := client.Namespaced(fqn) - oo, err := d.GetFactory().List(d.GVR(), ns, wait, labels.Everything()) + oo, err := d.getFactory().List(d.GVR(), ns, wait, labels.Everything()) if err != nil { return nil, err } @@ -194,7 +204,7 @@ func (d *Deployment) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs return nil, errors.New("expecting Deployment resource") } switch gvr { - case "v1/configmaps": + case CmGVR: if !hasConfigMap(&dp.Spec.Template.Spec, n) { continue } @@ -202,7 +212,7 @@ func (d *Deployment) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs GVR: d.GVR(), FQN: client.FQN(dp.Namespace, dp.Name), }) - case "v1/secrets": + case SecGVR: found, err := hasSecret(d.Factory, &dp.Spec.Template.Spec, dp.Namespace, n, wait) if err != nil { log.Warn().Err(err).Msgf("scanning secret %q", fqn) @@ -215,7 +225,7 @@ func (d *Deployment) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs GVR: d.GVR(), FQN: client.FQN(dp.Namespace, dp.Name), }) - case "v1/persistentvolumeclaims": + case PvcGVR: if !hasPVC(&dp.Spec.Template.Spec, n) { continue } @@ -223,7 +233,7 @@ func (d *Deployment) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs GVR: d.GVR(), FQN: client.FQN(dp.Namespace, dp.Name), }) - case "scheduling.k8s.io/v1/priorityclasses": + case PcGVR: if !hasPC(&dp.Spec.Template.Spec, n) { continue } @@ -240,7 +250,7 @@ func (d *Deployment) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs // GetPodSpec returns a pod spec given a resource. func (d *Deployment) GetPodSpec(path string) (*v1.PodSpec, error) { - dp, err := d.GetInstance(d.Factory, path) + dp, err := d.GetInstance(path) if err != nil { return nil, err } @@ -251,7 +261,7 @@ func (d *Deployment) GetPodSpec(path string) (*v1.PodSpec, error) { // SetImages sets container images. func (d *Deployment) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error { ns, n := client.Namespaced(path) - auth, err := d.Client().CanI(ns, "apps/v1/deployments", []string{client.PatchVerb}) + auth, err := d.Client().CanI(ns, "apps/v1/deployments", n, client.PatchAccess) if err != nil { return err } diff --git a/internal/dao/ds.go b/internal/dao/ds.go index 1ef7bf39a6..3bbd7827c0 100644 --- a/internal/dao/ds.go +++ b/internal/dao/ds.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -8,6 +11,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/watch" "github.com/rs/zerolog/log" appsv1 "k8s.io/api/apps/v1" @@ -29,6 +33,7 @@ var ( _ Restartable = (*DaemonSet)(nil) _ Controller = (*DaemonSet)(nil) _ ContainsPodSpec = (*DaemonSet)(nil) + _ ImageLister = (*DaemonSet)(nil) ) // DaemonSet represents a K8s daemonset. @@ -36,14 +41,19 @@ type DaemonSet struct { Resource } -// IsHappy check for happy deployments. -func (d *DaemonSet) IsHappy(ds appsv1.DaemonSet) bool { - return ds.Status.DesiredNumberScheduled == ds.Status.CurrentNumberScheduled +// ListImages lists container images. +func (d *DaemonSet) ListImages(ctx context.Context, fqn string) ([]string, error) { + ds, err := d.GetInstance(fqn) + if err != nil { + return nil, err + } + + return render.ExtractImages(&ds.Spec.Template.Spec), nil } // Restart a DaemonSet rollout. func (d *DaemonSet) Restart(ctx context.Context, path string) error { - o, err := d.GetFactory().Get("apps/v1/daemonsets", path, true, labels.Everything()) + o, err := d.getFactory().Get("apps/v1/daemonsets", path, true, labels.Everything()) if err != nil { return err } @@ -53,7 +63,7 @@ func (d *DaemonSet) Restart(ctx context.Context, path string) error { return err } - auth, err := d.Client().CanI(ds.Namespace, "apps/v1/daemonsets", []string{client.PatchVerb}) + auth, err := d.Client().CanI(ds.Namespace, "apps/v1/daemonsets", ds.Name, client.PatchAccess) if err != nil { return err } @@ -125,7 +135,7 @@ func podLogs(ctx context.Context, sel map[string]string, opts *LogOptions) ([]Lo } opts.MultiPods = true - po := Pod{} + var po Pod po.Init(f, client.NewGVR("v1/pods")) outs := make([]LogChan, 0, len(oo)) @@ -158,7 +168,7 @@ func (d *DaemonSet) Pod(fqn string) (string, error) { // GetInstance returns a daemonset instance. func (d *DaemonSet) GetInstance(fqn string) (*appsv1.DaemonSet, error) { - o, err := d.GetFactory().Get(d.gvr.String(), fqn, true, labels.Everything()) + o, err := d.getFactory().Get(d.gvrStr(), fqn, true, labels.Everything()) if err != nil { return nil, err } @@ -175,7 +185,7 @@ func (d *DaemonSet) GetInstance(fqn string) (*appsv1.DaemonSet, error) { // ScanSA scans for serviceaccount refs. func (d *DaemonSet) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) { ns, n := client.Namespaced(fqn) - oo, err := d.GetFactory().List(d.GVR(), ns, wait, labels.Everything()) + oo, err := d.getFactory().List(d.GVR(), ns, wait, labels.Everything()) if err != nil { return nil, err } @@ -199,9 +209,9 @@ func (d *DaemonSet) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, er } // Scan scans for cluster refs. -func (d *DaemonSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error) { +func (d *DaemonSet) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) { ns, n := client.Namespaced(fqn) - oo, err := d.GetFactory().List(d.GVR(), ns, wait, labels.Everything()) + oo, err := d.getFactory().List(d.GVR(), ns, wait, labels.Everything()) if err != nil { return nil, err } @@ -214,7 +224,7 @@ func (d *DaemonSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, return nil, errors.New("expecting StatefulSet resource") } switch gvr { - case "v1/configmaps": + case CmGVR: if !hasConfigMap(&ds.Spec.Template.Spec, n) { continue } @@ -222,7 +232,7 @@ func (d *DaemonSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, GVR: d.GVR(), FQN: client.FQN(ds.Namespace, ds.Name), }) - case "v1/secrets": + case SecGVR: found, err := hasSecret(d.Factory, &ds.Spec.Template.Spec, ds.Namespace, n, wait) if err != nil { log.Warn().Err(err).Msgf("locate secret %q", fqn) @@ -235,7 +245,7 @@ func (d *DaemonSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, GVR: d.GVR(), FQN: client.FQN(ds.Namespace, ds.Name), }) - case "v1/persistentvolumeclaims": + case PvcGVR: if !hasPVC(&ds.Spec.Template.Spec, n) { continue } @@ -243,7 +253,7 @@ func (d *DaemonSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, GVR: d.GVR(), FQN: client.FQN(ds.Namespace, ds.Name), }) - case "scheduling.k8s.io/v1/priorityclasses": + case PcGVR: if !hasPC(&ds.Spec.Template.Spec, n) { continue } @@ -270,7 +280,7 @@ func (d *DaemonSet) GetPodSpec(path string) (*v1.PodSpec, error) { // SetImages sets container images. func (d *DaemonSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error { ns, n := client.Namespaced(path) - auth, err := d.Client().CanI(ns, "apps/v1/daemonset", []string{client.PatchVerb}) + auth, err := d.Client().CanI(ns, "apps/v1/daemonset", n, client.PatchAccess) if err != nil { return err } diff --git a/internal/dao/generic.go b/internal/dao/generic.go index 22e4eebefd..e0aff2b649 100644 --- a/internal/dao/generic.go +++ b/internal/dao/generic.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -37,7 +40,7 @@ type Generic struct { func (g *Generic) List(ctx context.Context, ns string) ([]runtime.Object, error) { labelSel, _ := ctx.Value(internal.KeyLabels).(string) if client.IsAllNamespace(ns) { - ns = client.AllNamespaces + ns = client.BlankNamespace } var ( @@ -103,7 +106,7 @@ func (g *Generic) ToYAML(path string, showManaged bool) (string, error) { // Delete deletes a resource. func (g *Generic) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error { ns, n := client.Namespaced(path) - auth, err := g.Client().CanI(ns, g.gvr.String(), []string{client.DeleteVerb}) + auth, err := g.Client().CanI(ns, g.gvrStr(), n, []string{client.DeleteVerb}) if err != nil { return err } diff --git a/internal/dao/helm.go b/internal/dao/helm.go deleted file mode 100644 index 0c4ffd48c5..0000000000 --- a/internal/dao/helm.go +++ /dev/null @@ -1,143 +0,0 @@ -package dao - -import ( - "context" - "fmt" - "os" - - "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/render" - "github.com/rs/zerolog/log" - "gopkg.in/yaml.v2" - "helm.sh/helm/v3/pkg/action" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" -) - -var ( - _ Accessor = (*Helm)(nil) - _ Nuker = (*Helm)(nil) - _ Describer = (*Helm)(nil) -) - -// Helm represents a helm chart. -type Helm struct { - NonResource -} - -// List returns a collection of resources. -func (h *Helm) List(ctx context.Context, ns string) ([]runtime.Object, error) { - cfg, err := h.EnsureHelmConfig(ns) - if err != nil { - return nil, err - } - - list := action.NewList(cfg) - list.All = true - list.SetStateMask() - rr, err := list.Run() - if err != nil { - return nil, err - } - - oo := make([]runtime.Object, 0, len(rr)) - for _, r := range rr { - oo = append(oo, render.HelmRes{Release: r}) - } - - return oo, nil -} - -// Get returns a resource. -func (h *Helm) Get(_ context.Context, path string) (runtime.Object, error) { - ns, n := client.Namespaced(path) - cfg, err := h.EnsureHelmConfig(ns) - if err != nil { - return nil, err - } - resp, err := action.NewGet(cfg).Run(n) - if err != nil { - return nil, err - } - - return render.HelmRes{Release: resp}, nil -} - -// GetValues returns values for a release -func (h *Helm) GetValues(path string, allValues bool) ([]byte, error) { - ns, n := client.Namespaced(path) - cfg, err := h.EnsureHelmConfig(ns) - if err != nil { - return nil, err - } - vals := action.NewGetValues(cfg) - vals.AllValues = allValues - resp, err := vals.Run(n) - if err != nil { - return nil, err - } - - return yaml.Marshal(resp) -} - -// Describe returns the chart notes. -func (h *Helm) Describe(path string) (string, error) { - ns, n := client.Namespaced(path) - cfg, err := h.EnsureHelmConfig(ns) - if err != nil { - return "", err - } - resp, err := action.NewGet(cfg).Run(n) - if err != nil { - return "", err - } - - return resp.Info.Notes, nil -} - -// ToYAML returns the chart manifest. -func (h *Helm) ToYAML(path string, showManaged bool) (string, error) { - ns, n := client.Namespaced(path) - cfg, err := h.EnsureHelmConfig(ns) - if err != nil { - return "", err - } - resp, err := action.NewGet(cfg).Run(n) - if err != nil { - return "", err - } - - return resp.Manifest, nil -} - -// Delete uninstall a Helm. -func (h *Helm) Delete(_ context.Context, path string, _ *metav1.DeletionPropagation, _ Grace) error { - ns, n := client.Namespaced(path) - cfg, err := h.EnsureHelmConfig(ns) - if err != nil { - return err - } - u := action.NewUninstall(cfg) - u.KeepHistory = true - res, err := u.Run(n) - if err != nil { - return err - } - if res != nil && res.Info != "" { - return fmt.Errorf("%s", res.Info) - } - - return nil -} - -// EnsureHelmConfig return a new configuration. -func (h *Helm) EnsureHelmConfig(ns string) (*action.Configuration, error) { - cfg := new(action.Configuration) - err := cfg.Init(h.Client().Config().Flags(), ns, os.Getenv("HELM_DRIVER"), helmLogger) - - return cfg, err -} - -func helmLogger(s string, args ...interface{}) { - log.Debug().Msgf("%s %v", s, args) -} diff --git a/internal/dao/helm_chart.go b/internal/dao/helm_chart.go new file mode 100644 index 0000000000..2efe9f6b60 --- /dev/null +++ b/internal/dao/helm_chart.go @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package dao + +import ( + "context" + "fmt" + "os" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render/helm" + "github.com/rs/zerolog/log" + "gopkg.in/yaml.v2" + "helm.sh/helm/v3/pkg/action" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +var ( + _ Accessor = (*HelmChart)(nil) + _ Nuker = (*HelmChart)(nil) + _ Describer = (*HelmChart)(nil) + _ Valuer = (*HelmChart)(nil) +) + +// HelmChart represents a helm chart. +type HelmChart struct { + NonResource +} + +// List returns a collection of resources. +func (h *HelmChart) List(ctx context.Context, ns string) ([]runtime.Object, error) { + cfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns) + if err != nil { + return nil, err + } + + list := action.NewList(cfg) + list.All = true + list.SetStateMask() + rr, err := list.Run() + if err != nil { + return nil, err + } + + oo := make([]runtime.Object, 0, len(rr)) + for _, r := range rr { + oo = append(oo, helm.ReleaseRes{Release: r}) + } + + return oo, nil +} + +// Get returns a resource. +func (h *HelmChart) Get(_ context.Context, path string) (runtime.Object, error) { + ns, n := client.Namespaced(path) + cfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns) + if err != nil { + return nil, err + } + resp, err := action.NewGet(cfg).Run(n) + if err != nil { + return nil, err + } + + return helm.ReleaseRes{Release: resp}, nil +} + +// GetValues returns values for a release +func (h *HelmChart) GetValues(path string, allValues bool) ([]byte, error) { + ns, n := client.Namespaced(path) + cfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns) + if err != nil { + return nil, err + } + vals := action.NewGetValues(cfg) + vals.AllValues = allValues + resp, err := vals.Run(n) + if err != nil { + return nil, err + } + + return yaml.Marshal(resp) +} + +// Describe returns the chart notes. +func (h *HelmChart) Describe(path string) (string, error) { + ns, n := client.Namespaced(path) + cfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns) + if err != nil { + return "", err + } + resp, err := action.NewGet(cfg).Run(n) + if err != nil { + return "", err + } + + return resp.Info.Notes, nil +} + +// ToYAML returns the chart manifest. +func (h *HelmChart) ToYAML(path string, showManaged bool) (string, error) { + ns, n := client.Namespaced(path) + cfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns) + if err != nil { + return "", err + } + resp, err := action.NewGet(cfg).Run(n) + if err != nil { + return "", err + } + + return resp.Manifest, nil +} + +// Delete uninstall a HelmChart. +func (h *HelmChart) Delete(_ context.Context, path string, _ *metav1.DeletionPropagation, _ Grace) error { + return h.Uninstall(path, false) +} + +// Uninstall uninstalls a HelmChart. +func (h *HelmChart) Uninstall(path string, keepHist bool) error { + ns, n := client.Namespaced(path) + flags := h.Client().Config().Flags() + flags.Namespace = &ns + cfg, err := ensureHelmConfig(flags, ns) + if err != nil { + return err + } + + u := action.NewUninstall(cfg) + u.KeepHistory = keepHist + res, err := u.Run(n) + if err != nil { + return err + } + if res != nil && res.Info != "" { + return fmt.Errorf("%s", res.Info) + } + + return nil +} + +// ensureHelmConfig return a new configuration. +func ensureHelmConfig(flags *genericclioptions.ConfigFlags, ns string) (*action.Configuration, error) { + cfg := new(action.Configuration) + err := cfg.Init(flags, ns, os.Getenv("HELM_DRIVER"), helmLogger) + + return cfg, err +} + +func helmLogger(fmt string, args ...interface{}) { + log.Debug().Msgf("[Helm] "+fmt, args...) +} diff --git a/internal/dao/helm_history.go b/internal/dao/helm_history.go new file mode 100644 index 0000000000..3dd30deaea --- /dev/null +++ b/internal/dao/helm_history.go @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package dao + +import ( + "context" + "fmt" + "strconv" + "strings" + + "gopkg.in/yaml.v2" + "helm.sh/helm/v3/pkg/action" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render/helm" +) + +var ( + _ Accessor = (*HelmHistory)(nil) + _ Nuker = (*HelmHistory)(nil) + _ Describer = (*HelmHistory)(nil) + _ Valuer = (*HelmHistory)(nil) +) + +// HelmHistory represents a helm chart. +type HelmHistory struct { + NonResource +} + +// List returns a collection of resources. +func (h *HelmHistory) List(ctx context.Context, _ string) ([]runtime.Object, error) { + path, ok := ctx.Value(internal.KeyFQN).(string) + if !ok { + return nil, fmt.Errorf("expecting FQN in context") + } + ns, n := client.Namespaced(path) + + cfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns) + if err != nil { + return nil, err + } + + hh, err := action.NewHistory(cfg).Run(n) + if err != nil { + return nil, err + } + + oo := make([]runtime.Object, 0, len(hh)) + for _, r := range hh { + oo = append(oo, helm.ReleaseRes{Release: r}) + } + + return oo, nil +} + +// Get returns a resource. +func (h *HelmHistory) Get(_ context.Context, path string) (runtime.Object, error) { + fqn, rev, found := strings.Cut(path, ":") + if !found || len(rev) == 0 { + return nil, fmt.Errorf("invalid path %q", path) + } + + ns, n := client.Namespaced(fqn) + cfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns) + if err != nil { + return nil, err + } + + getter := action.NewGet(cfg) + getter.Version, err = strconv.Atoi(rev) + if err != nil { + return nil, err + } + + resp, err := getter.Run(n) + if err != nil { + return nil, err + } + + return helm.ReleaseRes{Release: resp}, nil +} + +// Describe returns the chart notes. +func (h *HelmHistory) Describe(path string) (string, error) { + rel, err := h.Get(context.Background(), path) + if err != nil { + return "", err + } + + resp, ok := rel.(helm.ReleaseRes) + if !ok { + return "", fmt.Errorf("expected helm.ReleaseRes, but got %T", rel) + } + + return resp.Release.Info.Notes, nil +} + +// ToYAML returns the chart manifest. +func (h *HelmHistory) ToYAML(path string, showManaged bool) (string, error) { + rel, err := h.Get(context.Background(), path) + if err != nil { + return "", err + } + + resp, ok := rel.(helm.ReleaseRes) + if !ok { + return "", fmt.Errorf("expected helm.ReleaseRes, but got %T", rel) + } + + return resp.Release.Manifest, nil +} + +// GetValues return the config for this chart. +func (h *HelmHistory) GetValues(path string, allValues bool) ([]byte, error) { + rel, err := h.Get(context.Background(), path) + if err != nil { + return nil, err + } + + resp, ok := rel.(helm.ReleaseRes) + if !ok { + return nil, fmt.Errorf("expected helm.ReleaseRes, but got %T", rel) + } + + if allValues { + return yaml.Marshal(resp.Release.Chart.Values) + } + return yaml.Marshal(resp.Release.Config) +} + +func (h *HelmHistory) Rollback(_ context.Context, path, rev string) error { + ns, n := client.Namespaced(path) + cfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns) + if err != nil { + return err + } + + ver, err := strconv.Atoi(rev) + if err != nil { + return fmt.Errorf("could not convert revision to a number: %w", err) + } + client := action.NewRollback(cfg) + client.Version = ver + + return client.Run(n) +} + +// Delete uninstall a Helm. +func (h *HelmHistory) Delete(_ context.Context, path string, _ *metav1.DeletionPropagation, _ Grace) error { + ns, n := client.Namespaced(path) + cfg, err := ensureHelmConfig(h.Client().Config().Flags(), ns) + if err != nil { + return err + } + + res, err := action.NewUninstall(cfg).Run(n) + if err != nil { + return err + } + + if res != nil && res.Info != "" { + return fmt.Errorf("%s", res.Info) + } + + return nil +} diff --git a/internal/dao/helpers.go b/internal/dao/helpers.go index c1b0a047f8..552d31378f 100644 --- a/internal/dao/helpers.go +++ b/internal/dao/helpers.go @@ -1,49 +1,70 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( "bytes" "errors" + "fmt" "math" - "regexp" - "github.com/derailed/tview" - runewidth "github.com/mattn/go-runewidth" + "github.com/derailed/k9s/internal/client" "github.com/rs/zerolog/log" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/printers" ) -const defaultServiceAccount = "default" - -var ( - inverseRx = regexp.MustCompile(`\A\!`) - fuzzyRx = regexp.MustCompile(`\A\-f`) +const ( + defaultServiceAccount = "default" + defaultContainerAnnotation = "kubectl.kubernetes.io/default-container" ) -func inList(ll []string, s string) bool { - for _, l := range ll { - if l == s { - return true +// GetDefaultContainer returns a container name if specified in an annotation. +func GetDefaultContainer(m metav1.ObjectMeta, spec v1.PodSpec) (string, bool) { + defaultContainer, ok := m.Annotations[defaultContainerAnnotation] + if !ok { + return "", false + } + + for _, container := range spec.Containers { + if container.Name == defaultContainer { + return defaultContainer, true } } - return false + log.Warn().Msg(defaultContainer + " container not found. " + defaultContainerAnnotation + " annotation will be ignored") + + return "", false } -// IsInverseSelector checks if inverse char has been provided. -func IsInverseSelector(s string) bool { - if s == "" { - return false +func extractFQN(o runtime.Object) string { + u, ok := o.(*unstructured.Unstructured) + if !ok { + log.Error().Err(fmt.Errorf("expecting unstructured but got %T", o)) + return client.NA } - return inverseRx.MatchString(s) + + return FQN(u.GetNamespace(), u.GetName()) } -// IsFuzzySelector checks if filter is fuzzy or not. -func IsFuzzySelector(s string) bool { - if s == "" { - return false +// FQN returns a fully qualified resource name. +func FQN(ns, n string) string { + if ns == "" { + return n } - return fuzzyRx.MatchString(s) + return ns + "/" + n +} + +func inList(ll []string, s string) bool { + for _, l := range ll { + if l == s { + return true + } + } + return false } func toPerc(v1, v2 float64) float64 { @@ -53,11 +74,6 @@ func toPerc(v1, v2 float64) float64 { return math.Round((v1 / v2) * 100) } -// Truncate a string to the given l and suffix ellipsis if needed. -func Truncate(str string, width int) string { - return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis)) -} - // ToYAML converts a resource to its YAML representation. func ToYAML(o runtime.Object, showManaged bool) (string, error) { if o == nil { @@ -93,3 +109,17 @@ func serviceAccountMatches(podSA, saName string) bool { } return podSA == saName } + +// ContinuousRanges takes a sorted slice of integers and returns a slice of +// sub-slices representing continuous ranges of integers. +func ContinuousRanges(indexes []int) [][]int { + var ranges [][]int + for i, p := 1, 0; i <= len(indexes); i++ { + if i == len(indexes) || indexes[i]-indexes[p] != i-p { + ranges = append(ranges, []int{indexes[p], indexes[i-1] + 1}) + p = i + } + } + + return ranges +} diff --git a/internal/dao/helpers_test.go b/internal/dao/helpers_test.go index 8687e119a9..f201654b51 100644 --- a/internal/dao/helpers_test.go +++ b/internal/dao/helpers_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -57,3 +60,35 @@ func TestServiceAccountMatches(t *testing.T) { assert.Equal(t, u.expect, serviceAccountMatches(u.podTemplate.ServiceAccountName, u.saName)) } } + +func TestContinuousRanges(t *testing.T) { + tests := []struct { + Indexes []int + Ranges [][]int + }{ + { + Indexes: []int{0}, + Ranges: [][]int{{0, 1}}, + }, + { + Indexes: []int{1}, + Ranges: [][]int{{1, 2}}, + }, + { + Indexes: []int{0, 1, 2}, + Ranges: [][]int{{0, 3}}, + }, + { + Indexes: []int{4, 5, 6}, + Ranges: [][]int{{4, 7}}, + }, + { + Indexes: []int{0, 2, 4, 5, 6}, + Ranges: [][]int{{0, 1}, {2, 3}, {4, 7}}, + }, + } + + for _, tt := range tests { + assert.Equal(t, tt.Ranges, ContinuousRanges(tt.Indexes)) + } +} diff --git a/internal/dao/img_scan.go b/internal/dao/img_scan.go new file mode 100644 index 0000000000..77cdd7b10d --- /dev/null +++ b/internal/dao/img_scan.go @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package dao + +import ( + "context" + "fmt" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/vul" + "k8s.io/apimachinery/pkg/runtime" +) + +var ( + _ Accessor = (*ImageScan)(nil) +) + +// ImageScan represents vulnerability scans. +type ImageScan struct { + NonResource +} + +func (is *ImageScan) listImages(ctx context.Context, gvr client.GVR, path string) ([]string, error) { + res, err := AccessorFor(is.Factory, gvr) + if err != nil { + return nil, err + } + s, ok := res.(ImageLister) + if !ok { + return nil, fmt.Errorf("resource %s is not image lister: %T", gvr, res) + } + + return s.ListImages(ctx, path) +} + +// List returns a collection of scans. +func (is *ImageScan) List(ctx context.Context, _ string) ([]runtime.Object, error) { + fqn, ok := ctx.Value(internal.KeyPath).(string) + if !ok { + return nil, fmt.Errorf("no context path for %q", is.gvr) + } + gvr, ok := ctx.Value(internal.KeyGVR).(client.GVR) + if !ok { + return nil, fmt.Errorf("no context gvr for %q", is.gvr) + } + + ii, err := is.listImages(ctx, gvr, fqn) + if err != nil { + return nil, err + } + + res := make([]runtime.Object, 0, len(ii)) + for _, img := range ii { + s, ok := vul.ImgScanner.GetScan(img) + if !ok { + continue + } + for _, r := range s.Table.Rows { + res = append(res, render.ImageScanRes{Image: img, Row: r}) + } + } + + return res, nil +} diff --git a/internal/dao/job.go b/internal/dao/job.go index 6264dff3ad..286cde59be 100644 --- a/internal/dao/job.go +++ b/internal/dao/job.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -7,6 +10,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" "github.com/rs/zerolog/log" batchv1 "k8s.io/api/batch/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -15,9 +19,10 @@ import ( ) var ( - _ Accessor = (*Job)(nil) - _ Nuker = (*Job)(nil) - _ Loggable = (*Job)(nil) + _ Accessor = (*Job)(nil) + _ Nuker = (*Job)(nil) + _ Loggable = (*Job)(nil) + _ ImageLister = (*Deployment)(nil) ) // Job represents a K8s job resource. @@ -25,6 +30,16 @@ type Job struct { Resource } +// ListImages lists container images. +func (j *Job) ListImages(ctx context.Context, fqn string) ([]string, error) { + job, err := j.GetInstance(fqn) + if err != nil { + return nil, err + } + + return render.ExtractImages(&job.Spec.Template.Spec), nil +} + // List returns a collection of resources. func (j *Job) List(ctx context.Context, ns string) ([]runtime.Object, error) { oo, err := j.Resource.List(ctx, ns) @@ -58,7 +73,7 @@ func (j *Job) List(ctx context.Context, ns string) ([]runtime.Object, error) { // TailLogs tail logs for all pods represented by this Job. func (j *Job) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) { - o, err := j.GetFactory().Get(j.gvr.String(), opts.Path, true, labels.Everything()) + o, err := j.getFactory().Get(j.gvrStr(), opts.Path, true, labels.Everything()) if err != nil { return nil, err } @@ -76,10 +91,25 @@ func (j *Job) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) return podLogs(ctx, job.Spec.Selector.MatchLabels, opts) } +func (j *Job) GetInstance(fqn string) (*batchv1.Job, error) { + o, err := j.getFactory().Get(j.gvrStr(), fqn, true, labels.Everything()) + if err != nil { + return nil, err + } + + var job batchv1.Job + err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &job) + if err != nil { + return nil, errors.New("expecting a job resource") + } + + return &job, nil +} + // ScanSA scans for serviceaccount refs. func (j *Job) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) { ns, n := client.Namespaced(fqn) - oo, err := j.GetFactory().List(j.GVR(), ns, wait, labels.Everything()) + oo, err := j.getFactory().List(j.GVR(), ns, wait, labels.Everything()) if err != nil { return nil, err } @@ -103,9 +133,9 @@ func (j *Job) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) { } // Scan scans for resource references. -func (j *Job) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error) { +func (j *Job) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) { ns, n := client.Namespaced(fqn) - oo, err := j.GetFactory().List(j.GVR(), ns, wait, labels.Everything()) + oo, err := j.getFactory().List(j.GVR(), ns, wait, labels.Everything()) if err != nil { return nil, err } @@ -118,7 +148,7 @@ func (j *Job) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error return nil, errors.New("expecting Job resource") } switch gvr { - case "v1/configmaps": + case CmGVR: if !hasConfigMap(&job.Spec.Template.Spec, n) { continue } @@ -126,7 +156,7 @@ func (j *Job) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error GVR: j.GVR(), FQN: client.FQN(job.Namespace, job.Name), }) - case "v1/secrets": + case SecGVR: found, err := hasSecret(j.Factory, &job.Spec.Template.Spec, job.Namespace, n, wait) if err != nil { log.Warn().Err(err).Msgf("locate secret %q", fqn) @@ -139,7 +169,7 @@ func (j *Job) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error GVR: j.GVR(), FQN: client.FQN(job.Namespace, job.Name), }) - case "scheduling.k8s.io/v1/priorityclasses": + case PcGVR: if !hasPC(&job.Spec.Template.Spec, n) { continue } diff --git a/internal/dao/log_item.go b/internal/dao/log_item.go index efea5ff70c..f20f2334e9 100644 --- a/internal/dao/log_item.go +++ b/internal/dao/log_item.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( diff --git a/internal/dao/log_item_test.go b/internal/dao/log_item_test.go index edfb7e15ec..b791f54432 100644 --- a/internal/dao/log_item_test.go +++ b/internal/dao/log_item_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao_test import ( diff --git a/internal/dao/log_items.go b/internal/dao/log_items.go index 4b8590724d..630df9e623 100644 --- a/internal/dao/log_items.go +++ b/internal/dao/log_items.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -7,6 +10,7 @@ import ( "strings" "sync" + "github.com/derailed/k9s/internal" "github.com/sahilm/fuzzy" ) @@ -171,8 +175,8 @@ func (l *LogItems) Filter(index int, q string, showTime bool, showJson bool) ([] if q == "" { return nil, nil, nil } - if IsFuzzySelector(q) { - mm, ii := l.fuzzyFilter(index, strings.TrimSpace(q[2:]), showTime, showJson) + if f, ok := internal.IsFuzzySelector(q); ok { + mm, ii := l.fuzzyFilter(index, f, showTime, showJson) return mm, ii, nil } matches, indices, err := l.filterLogs(index, q, showTime, showJson) @@ -197,7 +201,7 @@ func (l *LogItems) fuzzyFilter(index int, q string, showTime bool, showJson bool func (l *LogItems) filterLogs(index int, q string, showTime bool, showJson bool) ([]int, [][]int, error) { var invert bool - if IsInverseSelector(q) { + if internal.IsInverseSelector(q) { invert = true q = q[1:] } diff --git a/internal/dao/log_items_test.go b/internal/dao/log_items_test.go index 71fa733dc2..5e71debe9b 100644 --- a/internal/dao/log_items_test.go +++ b/internal/dao/log_items_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao_test import ( diff --git a/internal/dao/log_options.go b/internal/dao/log_options.go index c83e9e5657..c3c4cb3378 100644 --- a/internal/dao/log_options.go +++ b/internal/dao/log_options.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( diff --git a/internal/dao/log_options_test.go b/internal/dao/log_options_test.go index 71a594e648..feaaef8622 100644 --- a/internal/dao/log_options_test.go +++ b/internal/dao/log_options_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao_test import ( diff --git a/internal/dao/node.go b/internal/dao/node.go index 9504f110be..e55f40c5de 100644 --- a/internal/dao/node.go +++ b/internal/dao/node.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -53,7 +56,7 @@ func (n *Node) ToggleCordon(path string, cordon bool) error { } return fmt.Errorf("node is already uncordoned") } - dial, err := n.GetFactory().Client().Dial() + dial, err := n.getFactory().Client().Dial() if err != nil { return err } @@ -95,7 +98,7 @@ func (n *Node) Drain(path string, opts DrainOptions, w io.Writer) error { } } - dial, err := n.GetFactory().Client().Dial() + dial, err := n.getFactory().Client().Dial() if err != nil { return err } @@ -103,7 +106,7 @@ func (n *Node) Drain(path string, opts DrainOptions, w io.Writer) error { dd, errs := h.GetPodsForDeletion(path) if len(errs) != 0 { for _, e := range errs { - if _, err := h.ErrOut.Write([]byte(e.Error() + "\n")); err != nil { + if _, err := h.ErrOut.Write([]byte(fmt.Sprintf("[%s] %s\n", path, e.Error()))); err != nil { return err } } @@ -136,7 +139,7 @@ func (n *Node) Get(ctx context.Context, path string) (runtime.Object, error) { } var nmx *mv1beta1.NodeMetrics - if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok { + if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); ok && withMx { nmx, _ = client.DialMetrics(n.Client()).FetchNodeMetrics(ctx, path) } @@ -186,7 +189,7 @@ func (n *Node) List(ctx context.Context, ns string) ([]runtime.Object, error) { // CountPods counts the pods scheduled on a given node. func (n *Node) CountPods(nodeName string) (int, error) { var count int - oo, err := n.GetFactory().List("v1/pods", client.AllNamespaces, false, labels.Everything()) + oo, err := n.getFactory().List("v1/pods", client.BlankNamespace, false, labels.Everything()) if err != nil { return 0, err } @@ -210,7 +213,7 @@ func (n *Node) CountPods(nodeName string) (int, error) { // GetPods returns all pods running on given node. func (n *Node) GetPods(nodeName string) ([]*v1.Pod, error) { - oo, err := n.GetFactory().List("v1/pods", client.AllNamespaces, false, labels.Everything()) + oo, err := n.getFactory().List("v1/pods", client.BlankNamespace, false, labels.Everything()) if err != nil { return nil, err } @@ -244,7 +247,8 @@ func (n *Node) ensureCordoned(path string) (bool, error) { // FetchNode retrieves a node. func FetchNode(ctx context.Context, f Factory, path string) (*v1.Node, error) { - auth, err := f.Client().CanI(client.ClusterScope, "v1/nodes", []string{"get"}) + _, n := client.Namespaced(path) + auth, err := f.Client().CanI(client.ClusterScope, "v1/nodes", n, client.GetAccess) if err != nil { return nil, err } @@ -268,7 +272,7 @@ func FetchNode(ctx context.Context, f Factory, path string) (*v1.Node, error) { // FetchNodes retrieves all nodes. func FetchNodes(ctx context.Context, f Factory, labelsSel string) (*v1.NodeList, error) { - auth, err := f.Client().CanI(client.ClusterScope, "v1/nodes", []string{client.ListVerb}) + auth, err := f.Client().CanI(client.ClusterScope, "v1/nodes", "", client.ListAccess) if err != nil { return nil, err } diff --git a/internal/dao/non_resource.go b/internal/dao/non_resource.go index 96cf94b7ed..9840532ec4 100644 --- a/internal/dao/non_resource.go +++ b/internal/dao/non_resource.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -26,7 +29,14 @@ func (n *NonResource) Init(f Factory, gvr client.GVR) { n.mx.Unlock() } -func (n *NonResource) GetFactory() Factory { +func (n *NonResource) gvrStr() string { + n.mx.RLock() + defer n.mx.RUnlock() + + return n.gvr.String() +} + +func (n *NonResource) getFactory() Factory { n.mx.RLock() defer n.mx.RUnlock() @@ -38,7 +48,7 @@ func (n *NonResource) GVR() string { n.mx.RLock() defer n.mx.RUnlock() - return n.gvr.String() + return n.gvrStr() } // Get returns the given resource. diff --git a/internal/dao/ns.go b/internal/dao/ns.go index d9daddfca2..33a63dced0 100644 --- a/internal/dao/ns.go +++ b/internal/dao/ns.go @@ -1,26 +1,13 @@ -package dao - -import ( - "context" +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s - "k8s.io/apimachinery/pkg/runtime" -) +package dao var ( - _ Accessor = (*Pod)(nil) + _ Accessor = (*Namespace)(nil) ) // Namespace represents a namespace resource. type Namespace struct { - Generic -} - -// List returns a collection of nodes. -func (n *Namespace) List(ctx context.Context, ns string) ([]runtime.Object, error) { - oo, err := n.Generic.List(ctx, ns) - if err != nil { - return nil, err - } - - return oo, nil + Resource } diff --git a/internal/dao/ofaas.go b/internal/dao/ofaas.go index 4716a48466..0030aa4e85 100644 --- a/internal/dao/ofaas.go +++ b/internal/dao/ofaas.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao // BOZO!! Revamp with latest diff --git a/internal/dao/patch.go b/internal/dao/patch.go index 20fc8213ef..e834f196d0 100644 --- a/internal/dao/patch.go +++ b/internal/dao/patch.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( diff --git a/internal/dao/patch_test.go b/internal/dao/patch_test.go index 0fbda32a6b..efe460419f 100644 --- a/internal/dao/patch_test.go +++ b/internal/dao/patch_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( diff --git a/internal/dao/pod.go b/internal/dao/pod.go index a65e63d39f..c836137dd6 100644 --- a/internal/dao/pod.go +++ b/internal/dao/pod.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -30,12 +33,12 @@ var ( _ Loggable = (*Pod)(nil) _ Controller = (*Pod)(nil) _ ContainsPodSpec = (*Pod)(nil) + _ ImageLister = (*Pod)(nil) ) const ( - logRetryCount = 20 - logRetryWait = 1 * time.Second - defaultContainerAnnotation = "kubectl.kubernetes.io/default-container" + logRetryCount = 20 + logRetryWait = 1 * time.Second ) // Pod represents a pod resource. @@ -43,16 +46,6 @@ type Pod struct { Resource } -// IsHappy check for happy deployments. -func (p *Pod) IsHappy(po v1.Pod) bool { - for _, c := range po.Status.Conditions { - if c.Status == v1.ConditionFalse { - return false - } - } - return true -} - // Get returns a resource instance if found, else an error. func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) { o, err := p.Resource.Get(ctx, path) @@ -66,13 +59,23 @@ func (p *Pod) Get(ctx context.Context, path string) (runtime.Object, error) { } var pmx *mv1beta1.PodMetrics - if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok { + if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); ok && withMx { pmx, _ = client.DialMetrics(p.Client()).FetchPodMetrics(ctx, path) } return &render.PodWithMetrics{Raw: u, MX: pmx}, nil } +// ListImages lists container images. +func (p *Pod) ListImages(ctx context.Context, path string) ([]string, error) { + pod, err := p.GetInstance(path) + if err != nil { + return nil, err + } + + return render.ExtractImages(&pod.Spec), nil +} + // List returns a collection of nodes. func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) { oo, err := p.Resource.List(ctx, ns) @@ -81,7 +84,7 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) { } var pmx client.PodsMetricsMap - if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); withMx || !ok { + if withMx, ok := ctx.Value(internal.KeyWithMetrics).(bool); ok && withMx { pmx, _ = client.DialMetrics(p.Client()).FetchPodsMetricsMap(ctx, ns) } sel, _ := ctx.Value(internal.KeyFields).(string) @@ -117,8 +120,8 @@ func (p *Pod) List(ctx context.Context, ns string) ([]runtime.Object, error) { // Logs fetch container logs for a given pod and container. func (p *Pod) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, error) { - ns, _ := client.Namespaced(path) - auth, err := p.Client().CanI(ns, "v1/pods:log", []string{client.GetVerb}) + ns, n := client.Namespaced(path) + auth, err := p.Client().CanI(ns, "v1/pods:log", n, client.GetAccess) if err != nil { return nil, err } @@ -126,7 +129,6 @@ func (p *Pod) Logs(path string, opts *v1.PodLogOptions) (*restclient.Request, er return nil, fmt.Errorf("user is not authorized to view pod logs") } - ns, n := client.Namespaced(path) dial, err := p.Client().DialLogs() if err != nil { return nil, err @@ -163,7 +165,7 @@ func (p *Pod) Pod(fqn string) (string, error) { // GetInstance returns a pod instance. func (p *Pod) GetInstance(fqn string) (*v1.Pod, error) { - o, err := p.GetFactory().Get(p.gvr.String(), fqn, true, labels.Everything()) + o, err := p.getFactory().Get(p.gvrStr(), fqn, true, labels.Everything()) if err != nil { return nil, err } @@ -183,7 +185,7 @@ func (p *Pod) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) if !ok { return nil, errors.New("no factory in context") } - o, err := fac.Get(p.gvr.String(), opts.Path, true, labels.Everything()) + o, err := fac.Get(p.gvrStr(), opts.Path, true, labels.Everything()) if err != nil { return nil, err } @@ -205,19 +207,19 @@ func (p *Pod) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) return append(outs, tailLogs(ctx, p, opts)), nil } for _, co := range po.Spec.InitContainers { - o := opts.Clone() - o.Container = co.Name - outs = append(outs, tailLogs(ctx, p, o)) + cfg := opts.Clone() + cfg.Container = co.Name + outs = append(outs, tailLogs(ctx, p, cfg)) } for _, co := range po.Spec.Containers { - o := opts.Clone() - o.Container = co.Name - outs = append(outs, tailLogs(ctx, p, o)) + cfg := opts.Clone() + cfg.Container = co.Name + outs = append(outs, tailLogs(ctx, p, cfg)) } for _, co := range po.Spec.EphemeralContainers { - o := opts.Clone() - o.Container = co.Name - outs = append(outs, tailLogs(ctx, p, o)) + cfg := opts.Clone() + cfg.Container = co.Name + outs = append(outs, tailLogs(ctx, p, cfg)) } return outs, nil @@ -226,7 +228,7 @@ func (p *Pod) TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) // ScanSA scans for ServiceAccount refs. func (p *Pod) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) { ns, n := client.Namespaced(fqn) - oo, err := p.GetFactory().List(p.GVR(), ns, wait, labels.Everything()) + oo, err := p.getFactory().List(p.GVR(), ns, wait, labels.Everything()) if err != nil { return nil, err } @@ -254,9 +256,9 @@ func (p *Pod) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) { } // Scan scans for cluster resource refs. -func (p *Pod) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error) { +func (p *Pod) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) { ns, n := client.Namespaced(fqn) - oo, err := p.GetFactory().List(p.GVR(), ns, wait, labels.Everything()) + oo, err := p.getFactory().List(p.GVR(), ns, wait, labels.Everything()) if err != nil { return nil, err } @@ -273,7 +275,7 @@ func (p *Pod) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error continue } switch gvr { - case "v1/configmaps": + case CmGVR: if !hasConfigMap(&pod.Spec, n) { continue } @@ -281,7 +283,7 @@ func (p *Pod) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error GVR: p.GVR(), FQN: client.FQN(pod.Namespace, pod.Name), }) - case "v1/secrets": + case SecGVR: found, err := hasSecret(p.Factory, &pod.Spec, pod.Namespace, n, wait) if err != nil { log.Warn().Err(err).Msgf("locate secret %q", fqn) @@ -294,7 +296,7 @@ func (p *Pod) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error GVR: p.GVR(), FQN: client.FQN(pod.Namespace, pod.Name), }) - case "v1/persistentvolumeclaims": + case PvcGVR: if !hasPVC(&pod.Spec, n) { continue } @@ -302,7 +304,7 @@ func (p *Pod) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error GVR: p.GVR(), FQN: client.FQN(pod.Namespace, pod.Name), }) - case "scheduling.k8s.io/v1/priorityclasses": + case PcGVR: if !hasPC(&pod.Spec, n) { continue } @@ -411,24 +413,6 @@ func MetaFQN(m metav1.ObjectMeta) string { return FQN(m.Namespace, m.Name) } -// FQN returns a fully qualified resource name. -func FQN(ns, n string) string { - if ns == "" { - return n - } - return ns + "/" + n -} - -func extractFQN(o runtime.Object) string { - u, ok := o.(*unstructured.Unstructured) - if !ok { - log.Error().Err(fmt.Errorf("expecting unstructured but got %T", o)) - return client.NA - } - - return FQN(u.GetNamespace(), u.GetName()) -} - // GetPodSpec returns a pod spec given a resource. func (p *Pod) GetPodSpec(path string) (*v1.PodSpec, error) { pod, err := p.GetInstance(path) @@ -442,7 +426,7 @@ func (p *Pod) GetPodSpec(path string) (*v1.PodSpec, error) { // SetImages sets container images. func (p *Pod) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error { ns, n := client.Namespaced(path) - auth, err := p.Client().CanI(ns, "v1/pod", []string{client.PatchVerb}) + auth, err := p.Client().CanI(ns, "v1/pod", n, client.PatchAccess) if err != nil { return err } @@ -486,21 +470,6 @@ func (p *Pod) isControlled(path string) (string, bool, error) { return "", false, nil } -// GetDefaultContainer returns a container name if specified in an annotation. -func GetDefaultContainer(m metav1.ObjectMeta, spec v1.PodSpec) (string, bool) { - defaultContainer, ok := m.Annotations[defaultContainerAnnotation] - if ok { - for _, container := range spec.Containers { - if container.Name == defaultContainer { - return defaultContainer, true - } - } - log.Warn().Msg(defaultContainer + " container not found. " + defaultContainerAnnotation + " annotation will be ignored") - } - - return "", false -} - func (p *Pod) Sanitize(ctx context.Context, ns string) (int, error) { oo, err := p.Resource.List(ctx, ns) if err != nil { @@ -520,12 +489,27 @@ func (p *Pod) Sanitize(ctx context.Context, ns string) (int, error) { } log.Debug().Msgf("Pod status: %q", render.PodStatus(&pod)) switch render.PodStatus(&pod) { - case render.PhaseCompleted, render.PhaseCrashLoop, render.PhaseError, render.PhaseImagePullBackOff, render.PhaseOOMKilled: + case render.PhaseCompleted: + fallthrough + case render.PhasePending: + fallthrough + case render.PhaseCrashLoop: + fallthrough + case render.PhaseError: + fallthrough + case render.PhaseImagePullBackOff: + fallthrough + case render.PhaseContainerStatusUnknown: + fallthrough + case render.PhaseEvicted: + fallthrough + case render.PhaseOOMKilled: + // !!BOZO!! Might need to bump timeout otherwise rev limit if too many?? log.Debug().Msgf("Sanitizing %s:%s", pod.Namespace, pod.Name) fqn := client.FQN(pod.Namespace, pod.Name) - if err := p.Resource.Delete(ctx, fqn, nil, NowGrace); err != nil { - log.Warn().Err(err).Msgf("Pod %s deletion failed", fqn) - continue + if err := p.Delete(ctx, fqn, nil, 0); err != nil { + log.Debug().Msgf("Aborted! Sanitizer deleted %d pods", count) + return count, err } count++ } diff --git a/internal/dao/pod_test.go b/internal/dao/pod_test.go index cec9b794b9..2ec160b1cd 100644 --- a/internal/dao/pod_test.go +++ b/internal/dao/pod_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( diff --git a/internal/dao/popeye.go b/internal/dao/popeye.go index 7dab2be6a2..7cb08e9a27 100644 --- a/internal/dao/popeye.go +++ b/internal/dao/popeye.go @@ -1,139 +1,143 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "sort" - "time" - - "github.com/derailed/k9s/internal" - "github.com/derailed/k9s/internal/client" - cfg "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/render" - "github.com/derailed/popeye/pkg" - "github.com/derailed/popeye/pkg/config" - "github.com/derailed/popeye/types" - "github.com/rs/zerolog/log" - "k8s.io/apimachinery/pkg/runtime" -) - -var _ Accessor = (*Popeye)(nil) - -// Popeye tracks cluster sanitization. -type Popeye struct { - NonResource -} - -// NewPopeye returns a new set of aliases. -func NewPopeye(f Factory) *Popeye { - a := Popeye{} - a.Init(f, client.NewGVR("popeye")) - - return &a -} - -type readWriteCloser struct { - *bytes.Buffer -} - -// Close close read stream. -func (readWriteCloser) Close() error { - return nil -} - -// List returns a collection of aliases. -func (p *Popeye) List(ctx context.Context, ns string) ([]runtime.Object, error) { - defer func(t time.Time) { - log.Debug().Msgf("Popeye -- Elapsed %v", time.Since(t)) - if err := recover(); err != nil { - log.Debug().Msgf("POPEYE DIED!") - } - }(time.Now()) - - flags, js := config.NewFlags(), "json" - flags.Output = &js - flags.ActiveNamespace = &ns - - if report, ok := ctx.Value(internal.KeyPath).(string); ok && report != "" { - ns, n := client.Namespaced(report) - sections := []string{n} - flags.Sections = §ions - flags.ActiveNamespace = &ns - } - spinach := filepath.Join(cfg.K9sHome(), "spinach.yml") - if c, err := p.GetFactory().Client().Config().CurrentContextName(); err == nil { - spinach = filepath.Join(cfg.K9sHome(), fmt.Sprintf("%s_spinach.yml", c)) - } - if _, err := os.Stat(spinach); err == nil { - flags.Spinach = &spinach - } - - popeye, err := pkg.NewPopeye(flags, &log.Logger) - if err != nil { - return nil, err - } - popeye.SetFactory(newPopeyeFactory(p.Factory)) - if err = popeye.Init(); err != nil { - return nil, err - } - - buff := readWriteCloser{Buffer: bytes.NewBufferString("")} - popeye.SetOutputTarget(buff) - if _, _, err = popeye.Sanitize(); err != nil { - log.Error().Err(err).Msgf("BOOM %#v", *flags.Sections) - return nil, err - } - - var b render.Builder - if err = json.Unmarshal(buff.Bytes(), &b); err != nil { - return nil, err - } - - oo := make([]runtime.Object, 0, len(b.Report.Sections)) - sort.Sort(b.Report.Sections) - for _, s := range b.Report.Sections { - s.Tally.Count = len(s.Outcome) - if s.Tally.Sum() > 0 { - oo = append(oo, s) - } - } - - return oo, nil -} - -// Get retrieves a resource. -func (p *Popeye) Get(_ context.Context, _ string) (runtime.Object, error) { - return nil, errors.New("NYI!!") -} - -// ---------------------------------------------------------------------------- -// Helpers... - -type popFactory struct { - Factory -} - -var _ types.Factory = (*popFactory)(nil) - -func newPopeyeFactory(f Factory) *popFactory { - return &popFactory{Factory: f} -} - -func (p *popFactory) Client() types.Connection { - return &popeyeConnection{Connection: p.Factory.Client()} -} - -type popeyeConnection struct { - client.Connection -} - -var _ types.Connection = (*popeyeConnection)(nil) - -func (c *popeyeConnection) Config() types.Config { - return c.Connection.Config() -} +// !!BOZO!! Popeye +// import ( +// "bytes" +// "context" +// "encoding/json" +// "errors" +// "fmt" +// "os" +// "path/filepath" +// "sort" +// "time" + +// "github.com/derailed/k9s/internal" +// "github.com/derailed/k9s/internal/client" +// cfg "github.com/derailed/k9s/internal/config" +// "github.com/derailed/k9s/internal/render" +// "github.com/derailed/popeye/pkg" +// "github.com/derailed/popeye/pkg/config" +// "github.com/derailed/popeye/types" +// "github.com/rs/zerolog/log" +// "k8s.io/apimachinery/pkg/runtime" +// ) + +// var _ Accessor = (*Popeye)(nil) + +// // Popeye tracks cluster sanitization. +// type Popeye struct { +// NonResource +// } + +// // NewPopeye returns a new set of aliases. +// func NewPopeye(f Factory) *Popeye { +// a := Popeye{} +// a.Init(f, client.NewGVR("popeye")) + +// return &a +// } + +// type readWriteCloser struct { +// *bytes.Buffer +// } + +// // Close close read stream. +// func (readWriteCloser) Close() error { +// return nil +// } + +// // List returns a collection of aliases. +// func (p *Popeye) List(ctx context.Context, ns string) ([]runtime.Object, error) { +// defer func(t time.Time) { +// log.Debug().Msgf("Popeye -- Elapsed %v", time.Since(t)) +// if err := recover(); err != nil { +// log.Debug().Msgf("POPEYE DIED!") +// } +// }(time.Now()) + +// flags, js := config.NewFlags(), "json" +// flags.Output = &js +// flags.ActiveNamespace = &ns + +// if report, ok := ctx.Value(internal.KeyPath).(string); ok && report != "" { +// ns, n := client.Namespaced(report) +// sections := []string{n} +// flags.Sections = §ions +// flags.ActiveNamespace = &ns +// } +// spinach := filepath.Join(cfg.AppConfigDir, "spinach.yaml") +// if c, err := p.getFactory().Client().Config().CurrentContextName(); err == nil { +// spinach = filepath.Join(cfg.AppConfigDir, fmt.Sprintf("%s_spinach.yaml", c)) +// } +// if _, err := os.Stat(spinach); err == nil { +// flags.Spinach = &spinach +// } + +// popeye, err := pkg.NewPopeye(flags, &log.Logger) +// if err != nil { +// return nil, err +// } +// popeye.SetFactory(newPopeyeFactory(p.Factory)) +// if err = popeye.Init(); err != nil { +// return nil, err +// } + +// buff := readWriteCloser{Buffer: bytes.NewBufferString("")} +// popeye.SetOutputTarget(buff) +// if _, _, err = popeye.Sanitize(); err != nil { +// log.Error().Err(err).Msgf("BOOM %#v", *flags.Sections) +// return nil, err +// } + +// var b render.Builder +// if err = json.Unmarshal(buff.Bytes(), &b); err != nil { +// return nil, err +// } + +// oo := make([]runtime.Object, 0, len(b.Report.Sections)) +// sort.Sort(b.Report.Sections) +// for _, s := range b.Report.Sections { +// s.Tally.Count = len(s.Outcome) +// if s.Tally.Sum() > 0 { +// oo = append(oo, s) +// } +// } + +// return oo, nil +// } + +// // Get retrieves a resource. +// func (p *Popeye) Get(_ context.Context, _ string) (runtime.Object, error) { +// return nil, errors.New("NYI!!") +// } + +// // ---------------------------------------------------------------------------- +// // Helpers... + +// type popFactory struct { +// Factory +// } + +// var _ types.Factory = (*popFactory)(nil) + +// func newPopeyeFactory(f Factory) *popFactory { +// return &popFactory{Factory: f} +// } + +// func (p *popFactory) Client() types.Connection { +// return &popeyeConnection{Connection: p.Factory.Client()} +// } + +// type popeyeConnection struct { +// client.Connection +// } + +// var _ types.Connection = (*popeyeConnection)(nil) + +// func (c *popeyeConnection) Config() types.Config { +// return c.Connection.Config() +// } diff --git a/internal/dao/port_forward.go b/internal/dao/port_forward.go index 8e7e4c5465..3a0b6280fe 100644 --- a/internal/dao/port_forward.go +++ b/internal/dao/port_forward.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -27,7 +30,7 @@ type PortForward struct { // Delete deletes a portforward. func (p *PortForward) Delete(_ context.Context, path string, _ *metav1.DeletionPropagation, _ Grace) error { - p.GetFactory().DeleteForwarder(path) + p.getFactory().DeleteForwarder(path) return nil } @@ -35,17 +38,17 @@ func (p *PortForward) Delete(_ context.Context, path string, _ *metav1.DeletionP // List returns a collection of port forwards. func (p *PortForward) List(ctx context.Context, _ string) ([]runtime.Object, error) { benchFile, ok := ctx.Value(internal.KeyBenchCfg).(string) - if !ok { - return nil, fmt.Errorf("no bench file found in context") + if !ok || benchFile == "" { + return nil, fmt.Errorf("no benchmark config file found in context") } path, _ := ctx.Value(internal.KeyPath).(string) config, err := config.NewBench(benchFile) if err != nil { - log.Warn().Msgf("No custom benchmark config file found") + log.Debug().Msgf("No custom benchmark config file found: %q", benchFile) } - ff, cc := p.GetFactory().Forwarders(), config.Benchmarks.Containers + ff, cc := p.getFactory().Forwarders(), config.Benchmarks.Containers oo := make([]runtime.Object, 0, len(ff)) for k, f := range ff { if !strings.HasPrefix(k, path) { @@ -89,7 +92,7 @@ func BenchConfigFor(benchFile, path string) config.BenchConfig { def := config.DefaultBenchSpec() cust, err := config.NewBench(benchFile) if err != nil { - log.Debug().Msgf("No custom benchmark config file found") + log.Debug().Msgf("No custom benchmark config file found. Using default: %q", benchFile) return def } if b, ok := cust.Benchmarks.Containers[PodToKey(path)]; ok { diff --git a/internal/dao/port_forward_test.go b/internal/dao/port_forward_test.go index d5d757a379..2ce4de4234 100644 --- a/internal/dao/port_forward_test.go +++ b/internal/dao/port_forward_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao_test import ( @@ -14,7 +17,7 @@ func TestBenchForConfig(t *testing.T) { spec config.BenchConfig }{ "no_file": {file: "", key: "", spec: config.DefaultBenchSpec()}, - "spec": {file: "testdata/benchspec.yml", key: "default/nginx-123-456|nginx", spec: config.BenchConfig{ + "spec": {file: "testdata/benchspec.yaml", key: "default/nginx-123-456|nginx", spec: config.BenchConfig{ C: 2, N: 3000, HTTP: config.HTTP{ diff --git a/internal/dao/port_forwarder.go b/internal/dao/port_forwarder.go index 37812215c2..454af94285 100644 --- a/internal/dao/port_forwarder.go +++ b/internal/dao/port_forwarder.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -12,7 +15,6 @@ import ( "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/serializer" @@ -22,6 +24,8 @@ import ( "k8s.io/client-go/transport/spdy" ) +const defaultTimeout = 30 * time.Second + // PortForwarder tracks a port forward stream. type PortForwarder struct { Factory @@ -49,8 +53,8 @@ func (p *PortForwarder) String() string { } // Age returns the port forward age. -func (p *PortForwarder) Age() string { - return time.Since(p.age).String() +func (p *PortForwarder) Age() time.Time { + return p.age } // Active returns the forward status. @@ -92,7 +96,10 @@ func (p *PortForwarder) Container() string { func (p *PortForwarder) Stop() { log.Debug().Msgf("<<< Stopping PortForward %s", p.ID()) p.active = false - close(p.stopChan) + if p.stopChan != nil { + close(p.stopChan) + p.stopChan = nil + } } // FQN returns the portforward unique id. @@ -110,7 +117,7 @@ func (p *PortForwarder) Start(path string, tt port.PortTunnel) (*portforward.Por p.path, p.tunnel, p.age = path, tt, time.Now() ns, n := client.Namespaced(path) - auth, err := p.Client().CanI(ns, "v1/pods", []string{client.GetVerb}) + auth, err := p.Client().CanI(ns, "v1/pods", n, client.GetAccess) if err != nil { return nil, err } @@ -129,7 +136,7 @@ func (p *PortForwarder) Start(path string, tt port.PortTunnel) (*portforward.Por return nil, fmt.Errorf("unable to forward port because pod is not running. Current status=%v", pod.Status.Phase) } - auth, err = p.Client().CanI(ns, "v1/pods:portforward", []string{client.CreateVerb}) + auth, err = p.Client().CanI(ns, "v1/pods:portforward", "", []string{client.CreateVerb}) if err != nil { return nil, err } @@ -167,7 +174,7 @@ func (p *PortForwarder) forwardPorts(method string, url *url.URL, addr, portMap if err != nil { return nil, err } - dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport}, method, url) + dialer := spdy.NewDialer(upgrader, &http.Client{Transport: transport, Timeout: defaultTimeout}, method, url) return portforward.NewOnAddresses(dialer, []string{addr}, []string{portMap}, p.stopChan, p.readyChan, p.Out, p.ErrOut) } @@ -188,8 +195,8 @@ func codec() (serializer.CodecFactory, runtime.ParameterCodec) { scheme := runtime.NewScheme() gv := schema.GroupVersion{Group: "", Version: "v1"} metav1.AddToGroupVersion(scheme, gv) - scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) - scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) + scheme.AddKnownTypes(gv, &metav1.Table{}, &metav1.TableOptions{}) + scheme.AddKnownTypes(metav1.SchemeGroupVersion, &metav1.Table{}, &metav1.TableOptions{}) return serializer.NewCodecFactory(scheme), runtime.NewParameterCodec(scheme) } diff --git a/internal/dao/pulse.go b/internal/dao/pulse.go index 4c93ad38d7..b7d09c7b33 100644 --- a/internal/dao/pulse.go +++ b/internal/dao/pulse.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( diff --git a/internal/dao/rbac.go b/internal/dao/rbac.go index 540204a92e..7e688aee4d 100644 --- a/internal/dao/rbac.go +++ b/internal/dao/rbac.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -33,7 +36,7 @@ type Rbac struct { // List lists out rbac resources. func (r *Rbac) List(ctx context.Context, ns string) ([]runtime.Object, error) { - gvr, ok := ctx.Value(internal.KeyGVR).(string) + gvr, ok := ctx.Value(internal.KeyGVR).(client.GVR) if !ok { return nil, fmt.Errorf("expecting a context gvr") } @@ -42,8 +45,7 @@ func (r *Rbac) List(ctx context.Context, ns string) ([]runtime.Object, error) { return r.Resource.List(ctx, ns) } - res := client.NewGVR(gvr) - switch res.R() { + switch gvr.R() { case "clusterrolebindings": return r.loadClusterRoleBinding(path) case "rolebindings": @@ -53,12 +55,12 @@ func (r *Rbac) List(ctx context.Context, ns string) ([]runtime.Object, error) { case "roles": return r.loadRole(path) default: - return nil, fmt.Errorf("expecting clusterrole/role but found %s", res.R()) + return nil, fmt.Errorf("expecting clusterrole/role but found %s", gvr.R()) } } func (r *Rbac) loadClusterRoleBinding(path string) ([]runtime.Object, error) { - o, err := r.GetFactory().Get(crbGVR, path, true, labels.Everything()) + o, err := r.getFactory().Get(crbGVR, path, true, labels.Everything()) if err != nil { return nil, err } @@ -69,7 +71,7 @@ func (r *Rbac) loadClusterRoleBinding(path string) ([]runtime.Object, error) { return nil, err } - crbo, err := r.GetFactory().Get(crGVR, client.FQN("-", crb.RoleRef.Name), true, labels.Everything()) + crbo, err := r.getFactory().Get(crGVR, client.FQN("-", crb.RoleRef.Name), true, labels.Everything()) if err != nil { return nil, err } @@ -83,7 +85,7 @@ func (r *Rbac) loadClusterRoleBinding(path string) ([]runtime.Object, error) { } func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) { - o, err := r.GetFactory().Get(rbGVR, path, true, labels.Everything()) + o, err := r.getFactory().Get(rbGVR, path, true, labels.Everything()) if err != nil { return nil, err } @@ -94,7 +96,7 @@ func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) { } if rb.RoleRef.Kind == "ClusterRole" { - o, e := r.GetFactory().Get(crGVR, client.FQN("-", rb.RoleRef.Name), true, labels.Everything()) + o, e := r.getFactory().Get(crGVR, client.FQN("-", rb.RoleRef.Name), true, labels.Everything()) if e != nil { return nil, e } @@ -106,7 +108,7 @@ func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) { return asRuntimeObjects(parseRules(client.ClusterScope, "-", cr.Rules)), nil } - ro, err := r.GetFactory().Get(rGVR, client.FQN(rb.Namespace, rb.RoleRef.Name), true, labels.Everything()) + ro, err := r.getFactory().Get(rGVR, client.FQN(rb.Namespace, rb.RoleRef.Name), true, labels.Everything()) if err != nil { return nil, err } @@ -121,7 +123,7 @@ func (r *Rbac) loadRoleBinding(path string) ([]runtime.Object, error) { func (r *Rbac) loadClusterRole(path string) ([]runtime.Object, error) { log.Debug().Msgf("LOAD-CR %q", path) - o, err := r.GetFactory().Get(crGVR, path, true, labels.Everything()) + o, err := r.getFactory().Get(crGVR, path, true, labels.Everything()) if err != nil { return nil, err } @@ -136,7 +138,7 @@ func (r *Rbac) loadClusterRole(path string) ([]runtime.Object, error) { } func (r *Rbac) loadRole(path string) ([]runtime.Object, error) { - o, err := r.GetFactory().Get(rGVR, path, true, labels.Everything()) + o, err := r.getFactory().Get(rGVR, path, true, labels.Everything()) if err != nil { return nil, err } diff --git a/internal/dao/rbac_policy.go b/internal/dao/rbac_policy.go index a7f86543e2..7c901c227a 100644 --- a/internal/dao/rbac_policy.go +++ b/internal/dao/rbac_policy.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -178,13 +181,13 @@ func (p *Policy) fetchRoleBindingNamespaces(kind, name string) (map[string]strin // isSameSubject verifies if the incoming type name and namespace match a subject from a // cluster/roleBinding. A ServiceAccount will always have a namespace and needs to be validated to ensure // we don't display permissions for a ServiceAccount with the same name in a different namespace -func isSameSubject(kind, namespace, name string, subject *rbacv1.Subject) bool { +func isSameSubject(kind, ns, name string, subject *rbacv1.Subject) bool { if subject.Kind != kind || subject.Name != name { return false } if kind == rbacv1.ServiceAccountKind { // Kind and name were checked above, check the namespace - return subject.Namespace == namespace + return client.IsAllNamespaces(ns) || subject.Namespace == ns } return true } @@ -192,7 +195,7 @@ func isSameSubject(kind, namespace, name string, subject *rbacv1.Subject) bool { func (p *Policy) fetchClusterRoles() ([]rbacv1.ClusterRole, error) { const gvr = "rbac.authorization.k8s.io/v1/clusterroles" - oo, err := p.GetFactory().List(gvr, client.ClusterScope, false, labels.Everything()) + oo, err := p.getFactory().List(gvr, client.ClusterScope, false, labels.Everything()) if err != nil { return nil, err } @@ -212,7 +215,7 @@ func (p *Policy) fetchClusterRoles() ([]rbacv1.ClusterRole, error) { func (p *Policy) fetchRoles() ([]rbacv1.Role, error) { const gvr = "rbac.authorization.k8s.io/v1/roles" - oo, err := p.GetFactory().List(gvr, client.AllNamespaces, false, labels.Everything()) + oo, err := p.getFactory().List(gvr, client.BlankNamespace, false, labels.Everything()) if err != nil { return nil, err } diff --git a/internal/dao/rbac_policy_test.go b/internal/dao/rbac_policy_test.go index af49a10802..29967fdceb 100644 --- a/internal/dao/rbac_policy_test.go +++ b/internal/dao/rbac_policy_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( diff --git a/internal/dao/rbac_subject.go b/internal/dao/rbac_subject.go index 73f1392203..0939b50a62 100644 --- a/internal/dao/rbac_subject.go +++ b/internal/dao/rbac_subject.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( diff --git a/internal/dao/reference.go b/internal/dao/reference.go index d0331343aa..8ea76f5159 100644 --- a/internal/dao/reference.go +++ b/internal/dao/reference.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -19,12 +22,12 @@ type Reference struct { // List collects all references. func (r *Reference) List(ctx context.Context, ns string) ([]runtime.Object, error) { - gvr, ok := ctx.Value(internal.KeyGVR).(string) + gvr, ok := ctx.Value(internal.KeyGVR).(client.GVR) if !ok { return nil, errors.New("No context GVR found") } switch gvr { - case "v1/serviceaccounts": + case SaGVR: return r.ScanSA(ctx) default: return r.Scan(ctx) diff --git a/internal/dao/registry.go b/internal/dao/registry.go index 64638842c2..85416a752b 100644 --- a/internal/dao/registry.go +++ b/internal/dao/registry.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -12,53 +15,37 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" ) -// CRD identifies a CRD. -const CRD = "crd" +const ( + crdCat = "crd" + k9sCat = "k9s" + helmCat = "helm" + crdGVR = "apiextensions.k8s.io/v1/customresourcedefinitions" +) // MetaAccess tracks resources metadata. var MetaAccess = NewMeta() -var stdGroups = []string{ - "admissionregistration.k8s.io/v1", - "admissionregistration.k8s.io/v1beta1", - "apiextensions.k8s.io/v1", - "apiextensions.k8s.io/v1beta1", - "apiregistration.k8s.io/v1", - "apiregistration.k8s.io/v1beta1", - "apps/v1", - "authentication.k8s.io/v1", - "authentication.k8s.io/v1beta1", - "authorization.k8s.io/v1", - "authorization.k8s.io/v1beta1", - "autoscaling/v1", - "autoscaling/v2beta1", - "autoscaling/v2beta2", - "batch/v1", - "batch/v1beta1", - "certificates.k8s.io/v1", - "certificates.k8s.io/v1beta1", - "coordination.k8s.io/v1", - "coordination.k8s.io/v1beta1", - "discovery.k8s.io/v1beta1", - "dynatrace.com/v1alpha1", - "events.k8s.io/v1", - "extensions/v1beta1", - "flowcontrol.apiserver.k8s.io/v1beta1", - "metrics.k8s.io/v1beta1", - "networking.k8s.io/v1", - "networking.k8s.io/v1beta1", - "node.k8s.io/v1", - "node.k8s.io/v1beta1", - "policy/v1beta1", - "rbac.authorization.k8s.io/v1", - "rbac.authorization.k8s.io/v1beta1", - "scheduling.k8s.io/v1", - "scheduling.k8s.io/v1beta1", - "storage.k8s.io/v1", - "storage.k8s.io/v1beta1", - "v1", +var stdGroups = map[string]struct{}{ + "apps/v1": {}, + "autoscaling/v1": {}, + "autoscaling/v2": {}, + "autoscaling/v2beta1": {}, + "autoscaling/v2beta2": {}, + "batch/v1": {}, + "batch/v1beta1": {}, + "extensions/v1beta1": {}, + "policy/v1beta1": {}, + "policy/v1": {}, + "v1": {}, +} + +func (m ResourceMetas) clear() { + for k := range m { + delete(m, k) + } } // Meta represents available resource metas. @@ -77,32 +64,37 @@ func NewMeta() *Meta { // Customize here for non resource types or types with metrics or logs. func AccessorFor(f Factory, gvr client.GVR) (Accessor, error) { m := Accessors{ - client.NewGVR("contexts"): &Context{}, - client.NewGVR("containers"): &Container{}, - client.NewGVR("screendumps"): &ScreenDump{}, - client.NewGVR("benchmarks"): &Benchmark{}, - client.NewGVR("portforwards"): &PortForward{}, - client.NewGVR("v1/services"): &Service{}, - client.NewGVR("v1/pods"): &Pod{}, - client.NewGVR("v1/nodes"): &Node{}, - client.NewGVR("apps/v1/deployments"): &Deployment{}, - client.NewGVR("apps/v1/daemonsets"): &DaemonSet{}, - client.NewGVR("apps/v1/statefulsets"): &StatefulSet{}, - client.NewGVR("batch/v1/cronjobs"): &CronJob{}, - client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{}, - client.NewGVR("batch/v1/jobs"): &Job{}, - client.NewGVR("v1/namespaces"): &Namespace{}, - // BOZO!! Revamp with latest... - // client.NewGVR("openfaas"): &OpenFaas{}, - client.NewGVR("popeye"): &Popeye{}, - client.NewGVR("sanitizer"): &Popeye{}, - client.NewGVR("helm"): &Helm{}, - client.NewGVR("dir"): &Dir{}, + client.NewGVR("workloads"): &Workload{}, + client.NewGVR("contexts"): &Context{}, + client.NewGVR("containers"): &Container{}, + client.NewGVR("scans"): &ImageScan{}, + client.NewGVR("screendumps"): &ScreenDump{}, + client.NewGVR("benchmarks"): &Benchmark{}, + client.NewGVR("portforwards"): &PortForward{}, + client.NewGVR("dir"): &Dir{}, + client.NewGVR("v1/services"): &Service{}, + client.NewGVR("v1/pods"): &Pod{}, + client.NewGVR("v1/nodes"): &Node{}, + client.NewGVR("v1/namespaces"): &Namespace{}, + client.NewGVR("v1/configmaps"): &ConfigMap{}, + client.NewGVR("v1/secrets"): &Secret{}, + client.NewGVR("apps/v1/deployments"): &Deployment{}, + client.NewGVR("apps/v1/daemonsets"): &DaemonSet{}, + client.NewGVR("apps/v1/statefulsets"): &StatefulSet{}, + client.NewGVR("apps/v1/replicasets"): &ReplicaSet{}, + client.NewGVR("batch/v1/cronjobs"): &CronJob{}, + client.NewGVR("batch/v1beta1/cronjobs"): &CronJob{}, + client.NewGVR("batch/v1/jobs"): &Job{}, + client.NewGVR("helm"): &HelmChart{}, + client.NewGVR("helm-history"): &HelmHistory{}, + client.NewGVR("apiextensions.k8s.io/v1/customresourcedefinitions"): &CustomResourceDefinition{}, + // !!BOZO!! Popeye + //client.NewGVR("popeye"): &Popeye{}, } r, ok := m[gvr] if !ok { - r = &Generic{} + r = new(Generic) log.Debug().Msgf("No DAO registry entry for %q. Using generics!", gvr) } r.Init(f, gvr) @@ -132,10 +124,24 @@ func (m *Meta) AllGVRs() client.GVRs { return kk } +// GVK2GVR convert gvk to gvr +func (m *Meta) GVK2GVR(gv schema.GroupVersion, kind string) (client.GVR, bool, bool) { + m.mx.RLock() + defer m.mx.RUnlock() + + for gvr, meta := range m.resMetas { + if gv.Group == meta.Group && gv.Version == meta.Version && kind == meta.Kind { + return gvr, meta.Namespaced, true + } + } + + return client.NoGVR, false, false +} + // IsCRD checks if resource represents a CRD func IsCRD(r metav1.APIResource) bool { for _, c := range r.Categories { - if c == CRD { + if c == crdCat { return true } } @@ -157,7 +163,7 @@ func (m *Meta) MetaFor(gvr client.GVR) (metav1.APIResource, error) { // IsK8sMeta checks for non resource meta. func IsK8sMeta(m metav1.APIResource) bool { for _, c := range m.Categories { - if c == "k9s" || c == "helm" || c == "faas" { + if c == k9sCat || c == helmCat { return false } } @@ -168,7 +174,7 @@ func IsK8sMeta(m metav1.APIResource) bool { // IsK9sMeta checks for non resource meta. func IsK9sMeta(m metav1.APIResource) bool { for _, c := range m.Categories { - if c == "k9s" { + if c == k9sCat { return true } } @@ -181,7 +187,7 @@ func (m *Meta) LoadResources(f Factory) error { m.mx.Lock() defer m.mx.Unlock() - m.resMetas = make(ResourceMetas, 100) + m.resMetas.clear() if err := loadPreferred(f, m.resMetas); err != nil { return err } @@ -196,45 +202,49 @@ func loadNonResource(m ResourceMetas) { loadK9s(m) loadRBAC(m) loadHelm(m) - // BOZO!! Revamp with latest... - // if IsOpenFaasEnabled() { - // loadOpenFaas(m) - // } } func loadK9s(m ResourceMetas) { + m[client.NewGVR("workloads")] = metav1.APIResource{ + Name: "workloads", + Kind: "Workload", + SingularName: "workload", + Namespaced: true, + ShortNames: []string{"wk"}, + Categories: []string{k9sCat}, + } m[client.NewGVR("pulses")] = metav1.APIResource{ Name: "pulses", Kind: "Pulse", SingularName: "pulses", ShortNames: []string{"hz", "pu"}, - Categories: []string{"k9s"}, + Categories: []string{k9sCat}, } m[client.NewGVR("dir")] = metav1.APIResource{ Name: "dir", Kind: "Dir", SingularName: "dir", - Categories: []string{"k9s"}, + Categories: []string{k9sCat}, } m[client.NewGVR("xrays")] = metav1.APIResource{ Name: "xray", Kind: "XRays", SingularName: "xray", - Categories: []string{"k9s"}, + Categories: []string{k9sCat}, } m[client.NewGVR("references")] = metav1.APIResource{ Name: "references", Kind: "References", SingularName: "reference", Verbs: []string{}, - Categories: []string{"k9s"}, + Categories: []string{k9sCat}, } m[client.NewGVR("aliases")] = metav1.APIResource{ Name: "aliases", Kind: "Aliases", SingularName: "alias", Verbs: []string{}, - Categories: []string{"k9s"}, + Categories: []string{k9sCat}, } m[client.NewGVR("popeye")] = metav1.APIResource{ Name: "popeye", @@ -242,14 +252,14 @@ func loadK9s(m ResourceMetas) { SingularName: "popeye", Namespaced: true, Verbs: []string{}, - Categories: []string{"k9s"}, + Categories: []string{k9sCat}, } m[client.NewGVR("sanitizer")] = metav1.APIResource{ Name: "sanitizer", Kind: "Sanitizer", SingularName: "sanitizer", Verbs: []string{}, - Categories: []string{"k9s"}, + Categories: []string{k9sCat}, } m[client.NewGVR("contexts")] = metav1.APIResource{ Name: "contexts", @@ -257,7 +267,7 @@ func loadK9s(m ResourceMetas) { SingularName: "context", ShortNames: []string{"ctx"}, Verbs: []string{}, - Categories: []string{"k9s"}, + Categories: []string{k9sCat}, } m[client.NewGVR("screendumps")] = metav1.APIResource{ Name: "screendumps", @@ -265,7 +275,7 @@ func loadK9s(m ResourceMetas) { SingularName: "screendump", ShortNames: []string{"sd"}, Verbs: []string{"delete"}, - Categories: []string{"k9s"}, + Categories: []string{k9sCat}, } m[client.NewGVR("benchmarks")] = metav1.APIResource{ Name: "benchmarks", @@ -273,7 +283,7 @@ func loadK9s(m ResourceMetas) { SingularName: "benchmark", ShortNames: []string{"be"}, Verbs: []string{"delete"}, - Categories: []string{"k9s"}, + Categories: []string{k9sCat}, } m[client.NewGVR("portforwards")] = metav1.APIResource{ Name: "portforwards", @@ -282,14 +292,21 @@ func loadK9s(m ResourceMetas) { SingularName: "portforward", ShortNames: []string{"pf"}, Verbs: []string{"delete"}, - Categories: []string{"k9s"}, + Categories: []string{k9sCat}, } m[client.NewGVR("containers")] = metav1.APIResource{ Name: "containers", Kind: "Containers", SingularName: "container", Verbs: []string{}, - Categories: []string{"k9s"}, + Categories: []string{k9sCat}, + } + m[client.NewGVR("scans")] = metav1.APIResource{ + Name: "scans", + Kind: "Scans", + SingularName: "scan", + Verbs: []string{}, + Categories: []string{k9sCat}, } } @@ -299,43 +316,38 @@ func loadHelm(m ResourceMetas) { Kind: "Helm", Namespaced: true, Verbs: []string{"delete"}, - Categories: []string{"helm"}, + Categories: []string{helmCat}, + } + m[client.NewGVR("helm-history")] = metav1.APIResource{ + Name: "history", + Kind: "History", + Namespaced: true, + Verbs: []string{"delete"}, + Categories: []string{helmCat}, } } -// BOZO!! revamp with latest... -// func loadOpenFaas(m ResourceMetas) { -// m[client.NewGVR("openfaas")] = metav1.APIResource{ -// Name: "openfaas", -// Kind: "OpenFaaS", -// ShortNames: []string{"ofaas", "ofa"}, -// Namespaced: true, -// Verbs: []string{"delete"}, -// Categories: []string{"faas"}, -// } -// } - func loadRBAC(m ResourceMetas) { m[client.NewGVR("rbac")] = metav1.APIResource{ Name: "rbacs", Kind: "Rules", - Categories: []string{"k9s"}, + Categories: []string{k9sCat}, } m[client.NewGVR("policy")] = metav1.APIResource{ Name: "policies", Kind: "Rules", Namespaced: true, - Categories: []string{"k9s"}, + Categories: []string{k9sCat}, } m[client.NewGVR("users")] = metav1.APIResource{ Name: "users", Kind: "User", - Categories: []string{"k9s"}, + Categories: []string{k9sCat}, } m[client.NewGVR("groups")] = metav1.APIResource{ Name: "groups", Kind: "Group", - Categories: []string{"k9s"}, + Categories: []string{k9sCat}, } } @@ -363,8 +375,8 @@ func loadPreferred(f Factory, m ResourceMetas) error { if res.SingularName == "" { res.SingularName = strings.ToLower(res.Kind) } - if !isStandardGroup(res.Group) { - res.Categories = append(res.Categories, CRD) + if !isStandardGroup(r.GroupVersion) { + res.Categories = append(res.Categories, crdCat) } m[gvr] = res } @@ -373,14 +385,12 @@ func loadPreferred(f Factory, m ResourceMetas) error { return nil } -func isStandardGroup(r string) bool { - for _, res := range stdGroups { - if strings.Index(res, r) == 0 { - return true - } +func isStandardGroup(gv string) bool { + if _, ok := stdGroups[gv]; ok { + return true } - return false + return strings.Contains(gv, "k8s.io") } var deprecatedGVRs = map[client.GVR]struct{}{ @@ -396,7 +406,6 @@ func loadCRDs(f Factory, m ResourceMetas) { if f.Client() == nil || !f.Client().ConnectionOK() { return } - const crdGVR = "apiextensions.k8s.io/v1/customresourcedefinitions" oo, err := f.List(crdGVR, client.ClusterScope, false, labels.Everything()) if err != nil { log.Warn().Err(err).Msgf("Fail CRDs load") @@ -409,7 +418,7 @@ func loadCRDs(f Factory, m ResourceMetas) { log.Error().Err(errs[0]).Msgf("Fail to extract CRD meta (%d) errors", len(errs)) continue } - meta.Categories = append(meta.Categories, CRD) + meta.Categories = append(meta.Categories, crdCat) gvr := client.NewGVRFromMeta(meta) m[gvr] = meta } @@ -423,7 +432,7 @@ func extractMeta(o runtime.Object) (metav1.APIResource, []error) { crd, ok := o.(*unstructured.Unstructured) if !ok { - return m, append(errs, fmt.Errorf("Expected Unstructured, but got %T", o)) + return m, append(errs, fmt.Errorf("expected unstructured, but got %T", o)) } var spec map[string]interface{} diff --git a/internal/dao/registry_test.go b/internal/dao/registry_test.go index 4d28895bd4..51a05dba08 100644 --- a/internal/dao/registry_test.go +++ b/internal/dao/registry_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( diff --git a/internal/dao/resource.go b/internal/dao/resource.go index 18ea41881e..d704c4b5df 100644 --- a/internal/dao/resource.go +++ b/internal/dao/resource.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -30,12 +33,12 @@ func (r *Resource) List(ctx context.Context, ns string) ([]runtime.Object, error } } - return r.GetFactory().List(r.gvr.String(), ns, false, lsel) + return r.getFactory().List(r.gvrStr(), ns, false, lsel) } // Get returns a resource instance if found, else an error. func (r *Resource) Get(_ context.Context, path string) (runtime.Object, error) { - return r.GetFactory().Get(r.gvr.String(), path, true, labels.Everything()) + return r.getFactory().Get(r.gvrStr(), path, true, labels.Everything()) } // ToYAML returns a resource yaml. diff --git a/internal/dao/rest_mapper.go b/internal/dao/rest_mapper.go index 3220fe0a79..d9e6f8e3cf 100644 --- a/internal/dao/rest_mapper.go +++ b/internal/dao/rest_mapper.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -25,7 +28,7 @@ func (r *RestMapper) ToRESTMapper() (meta.RESTMapper, error) { return nil, err } mapper := restmapper.NewDeferredDiscoveryRESTMapper(dial) - expander := restmapper.NewShortcutExpander(mapper, dial) + expander := restmapper.NewShortcutExpander(mapper, dial, nil) return expander, nil } diff --git a/internal/dao/rs.go b/internal/dao/rs.go index 7e7ae755b5..fea9d3265d 100644 --- a/internal/dao/rs.go +++ b/internal/dao/rs.go @@ -1,12 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( + "context" "errors" "fmt" "strconv" "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" @@ -16,11 +21,25 @@ import ( "k8s.io/kubectl/pkg/polymorphichelpers" ) +var ( + _ ImageLister = (*ReplicaSet)(nil) +) + // ReplicaSet represents a replicaset K8s resource. type ReplicaSet struct { Resource } +// ListImages lists container images. +func (r *ReplicaSet) ListImages(ctx context.Context, fqn string) ([]string, error) { + rs, err := r.Load(r.Factory, fqn) + if err != nil { + return nil, err + } + + return render.ExtractImages(&rs.Spec.Template.Spec), nil +} + // Load returns a given instance. func (r *ReplicaSet) Load(f Factory, path string) (*appsv1.ReplicaSet, error) { o, err := f.Get("apps/v1/replicasets", path, true, labels.Everything()) @@ -95,7 +114,8 @@ func (r *ReplicaSet) Rollback(fqn string) error { } var ddp Deployment - dp, err := ddp.GetInstance(r.Factory, client.FQN(rs.Namespace, name)) + ddp.Init(r.Factory, client.NewGVR("apps/v1/deployments")) + dp, err := ddp.GetInstance(client.FQN(rs.Namespace, name)) if err != nil { return err } diff --git a/internal/dao/screen_dump.go b/internal/dao/screen_dump.go index 7ca9962684..18b304d3ec 100644 --- a/internal/dao/screen_dump.go +++ b/internal/dao/screen_dump.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( diff --git a/internal/dao/secret.go b/internal/dao/secret.go new file mode 100644 index 0000000000..49dcaa3000 --- /dev/null +++ b/internal/dao/secret.go @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package dao + +import ( + "fmt" + "strings" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" +) + +// Secret represents a secret K8s resource. +type Secret struct { + Resource + decode bool +} + +// Describe describes a secret that can be encoded or decoded. +func (s *Secret) Describe(path string) (string, error) { + encodedDescription, err := s.Generic.Describe(path) + + if err != nil { + return "", err + } + + if !s.decode { + return encodedDescription, nil + } + + return s.Decode(encodedDescription, path) +} + +// SetDecode sets the decode flag. +func (s *Secret) SetDecode(flag bool) { + s.decode = flag +} + +// Decode removes the encoded part from the secret's description and appends the +// secret's decoded data. +func (s *Secret) Decode(encodedDescription, path string) (string, error) { + o, err := s.getFactory().Get(s.GVR(), path, true, labels.Everything()) + + if err != nil { + return "", err + } + + dataEndIndex := strings.Index(encodedDescription, "====") + + if dataEndIndex == -1 { + return "", fmt.Errorf("unable to find data section in secret description") + } + + dataEndIndex += 4 + + if dataEndIndex >= len(encodedDescription) { + return "", fmt.Errorf("data section in secret description is invalid") + } + + // Remove the encoded part from k8s's describe API + // More details about the reasoning of index: https://github.com/kubernetes/kubectl/blob/v0.29.0/pkg/describe/describe.go#L2542 + body := encodedDescription[0:dataEndIndex] + + d, err := ExtractSecrets(o.(*unstructured.Unstructured)) + + if err != nil { + return "", err + } + + decodedSecrets := []string{} + + for k, v := range d { + decodedSecrets = append(decodedSecrets, "\n", k, ":\t", v) + } + + return body + strings.Join(decodedSecrets, ""), nil +} + +// ExtractSecrets takes an unstructured object and attempts to convert it into a +// Kubernetes Secret. +// It returns a map where the keys are the secret data keys and the values are +// the corresponding secret data values. +// If the conversion fails, it returns an error. +func ExtractSecrets(o *unstructured.Unstructured) (map[string]string, error) { + var secret v1.Secret + err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.Object, &secret) + + if err != nil { + return nil, err + } + + secretData := make(map[string]string, len(secret.Data)) + + for k, val := range secret.Data { + secretData[k] = string(val) + } + + return secretData, nil +} diff --git a/internal/dao/secret_test.go b/internal/dao/secret_test.go new file mode 100644 index 0000000000..4602ec8580 --- /dev/null +++ b/internal/dao/secret_test.go @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package dao_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/stretchr/testify/assert" +) + +func TestEncodedSecretDescribe(t *testing.T) { + s := dao.Secret{} + s.Init(makeFactory(), client.NewGVR("v1/secrets")) + + encodedString := + ` +Name: bootstrap-token-abcdef +Namespace: kube-system +Labels: +Annotations: + +Type: generic + +Data +==== +token-secret: 24 bytes` + + expected := "\nName: bootstrap-token-abcdef\n" + + "Namespace: kube-system\n" + + "Labels: \n" + + "Annotations: \n" + + "\n" + + "Type: generic\n" + + "\n" + + "Data\n" + + "====\n" + + "token-secret:\t0123456789abcdef" + + decodedDescription, _ := s.Decode(encodedString, "kube-system/bootstrap-token-abcdef") + assert.Equal(t, expected, decodedDescription) +} diff --git a/internal/dao/sts.go b/internal/dao/sts.go index 8e346233b1..9c111d523f 100644 --- a/internal/dao/sts.go +++ b/internal/dao/sts.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -7,6 +10,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" "github.com/rs/zerolog/log" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" @@ -28,6 +32,7 @@ var ( _ Scalable = (*StatefulSet)(nil) _ Controller = (*StatefulSet)(nil) _ ContainsPodSpec = (*StatefulSet)(nil) + _ ImageLister = (*StatefulSet)(nil) ) // StatefulSet represents a K8s sts. @@ -35,15 +40,20 @@ type StatefulSet struct { Resource } -// IsHappy check for happy sts. -func (s *StatefulSet) IsHappy(sts appsv1.StatefulSet) bool { - return sts.Status.Replicas == sts.Status.ReadyReplicas +// ListImages lists container images. +func (s *StatefulSet) ListImages(ctx context.Context, fqn string) ([]string, error) { + sts, err := s.GetInstance(s.Factory, fqn) + if err != nil { + return nil, err + } + + return render.ExtractImages(&sts.Spec.Template.Spec), nil } // Scale a StatefulSet. func (s *StatefulSet) Scale(ctx context.Context, path string, replicas int32) error { ns, n := client.Namespaced(path) - auth, err := s.Client().CanI(ns, "apps/v1/statefulsets:scale", []string{client.GetVerb, client.UpdateVerb}) + auth, err := s.Client().CanI(ns, "apps/v1/statefulsets:scale", n, []string{client.GetVerb, client.UpdateVerb}) if err != nil { return err } @@ -72,7 +82,7 @@ func (s *StatefulSet) Restart(ctx context.Context, path string) error { return err } - ns, _ := client.Namespaced(path) + ns, n := client.Namespaced(path) pp, err := podsFromSelector(s.Factory, ns, sts.Spec.Selector.MatchLabels) if err != nil { return err @@ -81,7 +91,7 @@ func (s *StatefulSet) Restart(ctx context.Context, path string) error { s.Forwarders().Kill(client.FQN(p.Namespace, p.Name)) } - auth, err := s.Client().CanI(sts.Namespace, "apps/v1/statefulsets", []string{client.PatchVerb}) + auth, err := s.Client().CanI(sts.Namespace, "apps/v1/statefulsets", n, client.PatchAccess) if err != nil { return err } @@ -159,7 +169,7 @@ func (s *StatefulSet) Pod(fqn string) (string, error) { } func (s *StatefulSet) getStatefulSet(fqn string) (*appsv1.StatefulSet, error) { - o, err := s.GetFactory().Get(s.gvr.String(), fqn, true, labels.Everything()) + o, err := s.getFactory().Get(s.gvrStr(), fqn, true, labels.Everything()) if err != nil { return nil, err } @@ -176,7 +186,7 @@ func (s *StatefulSet) getStatefulSet(fqn string) (*appsv1.StatefulSet, error) { // ScanSA scans for serviceaccount refs. func (s *StatefulSet) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, error) { ns, n := client.Namespaced(fqn) - oo, err := s.GetFactory().List(s.GVR(), ns, wait, labels.Everything()) + oo, err := s.getFactory().List(s.GVR(), ns, wait, labels.Everything()) if err != nil { return nil, err } @@ -200,9 +210,9 @@ func (s *StatefulSet) ScanSA(ctx context.Context, fqn string, wait bool) (Refs, } // Scan scans for cluster resource refs. -func (s *StatefulSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Refs, error) { +func (s *StatefulSet) Scan(ctx context.Context, gvr client.GVR, fqn string, wait bool) (Refs, error) { ns, n := client.Namespaced(fqn) - oo, err := s.GetFactory().List(s.GVR(), ns, wait, labels.Everything()) + oo, err := s.getFactory().List(s.GVR(), ns, wait, labels.Everything()) if err != nil { return nil, err } @@ -215,7 +225,7 @@ func (s *StatefulSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Ref return nil, errors.New("expecting StatefulSet resource") } switch gvr { - case "v1/configmaps": + case CmGVR: if !hasConfigMap(&sts.Spec.Template.Spec, n) { continue } @@ -223,7 +233,7 @@ func (s *StatefulSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Ref GVR: s.GVR(), FQN: client.FQN(sts.Namespace, sts.Name), }) - case "v1/secrets": + case SecGVR: found, err := hasSecret(s.Factory, &sts.Spec.Template.Spec, sts.Namespace, n, wait) if err != nil { log.Warn().Err(err).Msgf("locate secret %q", fqn) @@ -236,7 +246,7 @@ func (s *StatefulSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Ref GVR: s.GVR(), FQN: client.FQN(sts.Namespace, sts.Name), }) - case "v1/persistentvolumeclaims": + case PvcGVR: for _, v := range sts.Spec.VolumeClaimTemplates { if !strings.HasPrefix(n, v.Name+"-"+sts.Name) { continue @@ -253,7 +263,7 @@ func (s *StatefulSet) Scan(ctx context.Context, gvr, fqn string, wait bool) (Ref GVR: s.GVR(), FQN: client.FQN(sts.Namespace, sts.Name), }) - case "scheduling.k8s.io/v1/priorityclasses": + case PcGVR: if !hasPC(&sts.Spec.Template.Spec, n) { continue } @@ -281,7 +291,7 @@ func (s *StatefulSet) GetPodSpec(path string) (*v1.PodSpec, error) { // SetImages sets container images. func (s *StatefulSet) SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error { ns, n := client.Namespaced(path) - auth, err := s.Client().CanI(ns, "apps/v1/statefulset", []string{client.PatchVerb}) + auth, err := s.Client().CanI(ns, "apps/v1/statefulset", n, client.PatchAccess) if err != nil { return err } diff --git a/internal/dao/svc.go b/internal/dao/svc.go index 55023c4369..1e8fcfe877 100644 --- a/internal/dao/svc.go +++ b/internal/dao/svc.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -48,7 +51,7 @@ func (s *Service) Pod(fqn string) (string, error) { // GetInstance returns a service instance. func (s *Service) GetInstance(fqn string) (*v1.Service, error) { - o, err := s.GetFactory().Get(s.gvr.String(), fqn, true, labels.Everything()) + o, err := s.getFactory().Get(s.gvrStr(), fqn, true, labels.Everything()) if err != nil { return nil, err } diff --git a/internal/dao/table.go b/internal/dao/table.go index 422ccdddb4..6936772f82 100644 --- a/internal/dao/table.go +++ b/internal/dao/table.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -7,13 +10,15 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/client-go/rest" ) -// BOZO!! Figure out how to convert to table def and use factory. +const gvFmt = "application/json;as=Table;v=%s;g=%s, application/json" + +var genScheme = runtime.NewScheme() // Table retrieves K8s resources as tabular data. type Table struct { @@ -22,19 +27,19 @@ type Table struct { // Get returns a given resource. func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) { - a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName) - _, codec := t.codec() - - c, err := t.getClient() + f, p := t.codec() + c, err := t.getClient(f) if err != nil { return nil, err } + ns, n := client.Namespaced(path) + a := fmt.Sprintf(gvFmt, metav1.SchemeGroupVersion.Version, metav1.GroupName) req := c.Get(). SetHeader("Accept", a). Name(n). Resource(t.gvr.R()). - VersionedParams(&metav1beta1.TableOptions{}, codec) + VersionedParams(&metav1.TableOptions{}, p) if ns != client.ClusterScope { req = req.Namespace(ns) } @@ -44,23 +49,25 @@ func (t *Table) Get(ctx context.Context, path string) (runtime.Object, error) { // List all Resources in a given namespace. func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) { - labelSel, ok := ctx.Value(internal.KeyLabels).(string) - if !ok { - labelSel = "" - } - - a := fmt.Sprintf(gvFmt, metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName) - _, codec := t.codec() + labelSel, _ := ctx.Value(internal.KeyLabels).(string) + fieldSel, _ := ctx.Value(internal.KeyFields).(string) - c, err := t.getClient() + f, p := t.codec() + c, err := t.getClient(f) if err != nil { return nil, err } + a := fmt.Sprintf(gvFmt, metav1.SchemeGroupVersion.Version, metav1.GroupName) o, err := c.Get(). SetHeader("Accept", a). Namespace(ns). Resource(t.gvr.R()). - VersionedParams(&metav1.ListOptions{LabelSelector: labelSel}, codec). + VersionedParams(&metav1.ListOptions{ + LabelSelector: labelSel, + FieldSelector: fieldSel, + ResourceVersion: "0", + ResourceVersionMatch: v1.ResourceVersionMatchNotOlderThan, + }, p). Do(ctx).Get() if err != nil { return nil, err @@ -72,9 +79,7 @@ func (t *Table) List(ctx context.Context, ns string) ([]runtime.Object, error) { // ---------------------------------------------------------------------------- // Helpers... -const gvFmt = "application/json;as=Table;v=%s;g=%s, application/json" - -func (t *Table) getClient() (*rest.RESTClient, error) { +func (t *Table) getClient(f serializer.CodecFactory) (*rest.RESTClient, error) { cfg, err := t.Client().RestConfig() if err != nil { return nil, err @@ -85,8 +90,7 @@ func (t *Table) getClient() (*rest.RESTClient, error) { if t.gvr.G() == "" { cfg.APIPath = "/api" } - codec, _ := t.codec() - cfg.NegotiatedSerializer = codec.WithoutConversion() + cfg.NegotiatedSerializer = f.WithoutConversion() crRestClient, err := rest.RESTClientFor(cfg) if err != nil { @@ -97,11 +101,12 @@ func (t *Table) getClient() (*rest.RESTClient, error) { } func (t *Table) codec() (serializer.CodecFactory, runtime.ParameterCodec) { - scheme := runtime.NewScheme() + var tt metav1.Table + opts := metav1.TableOptions{IncludeObject: v1.IncludeObject} gv := t.gvr.GV() - metav1.AddToGroupVersion(scheme, gv) - scheme.AddKnownTypes(gv, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) - scheme.AddKnownTypes(metav1beta1.SchemeGroupVersion, &metav1beta1.Table{}, &metav1beta1.TableOptions{}) + metav1.AddToGroupVersion(genScheme, gv) + genScheme.AddKnownTypes(gv, &tt, &opts) + genScheme.AddKnownTypes(metav1.SchemeGroupVersion, &tt, &opts) - return serializer.NewCodecFactory(scheme), runtime.NewParameterCodec(scheme) + return serializer.NewCodecFactory(genScheme), runtime.NewParameterCodec(genScheme) } diff --git a/internal/dao/testdata/benchspec.yml b/internal/dao/testdata/benchspec.yaml similarity index 100% rename from internal/dao/testdata/benchspec.yml rename to internal/dao/testdata/benchspec.yaml diff --git a/internal/config/testdata/empty_skin.yml b/internal/dao/testdata/dir/a.yaml similarity index 100% rename from internal/config/testdata/empty_skin.yml rename to internal/dao/testdata/dir/a.yaml diff --git a/internal/dao/testdata/dir/a.yml b/internal/dao/testdata/dir/a/b.yaml similarity index 100% rename from internal/dao/testdata/dir/a.yml rename to internal/dao/testdata/dir/a/b.yaml diff --git a/internal/dao/testdata/dir/a/b.yml b/internal/dao/testdata/dir/a/b.yml deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/internal/dao/testdata/secret.json b/internal/dao/testdata/secret.json new file mode 100644 index 0000000000..fa560690ae --- /dev/null +++ b/internal/dao/testdata/secret.json @@ -0,0 +1,15 @@ +{ + "apiVersion": "v1", + "data": { + "token-secret": "MDEyMzQ1Njc4OWFiY2RlZg==" + }, + "kind": "Secret", + "metadata": { + "creationTimestamp": "2024-01-15T18:19:00Z", + "name": "bootstrap-token-abcdef", + "namespace": "kube-system", + "resourceVersion": "243", + "uid": "6f5695d4-c0f4-4b65-890a-b1115ffd1f3b" + }, + "type": "bootstrap.kubernetes.io/token" +} diff --git a/internal/dao/types.go b/internal/dao/types.go index 23ac9e7221..da6fc7ff53 100644 --- a/internal/dao/types.go +++ b/internal/dao/types.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dao import ( @@ -44,10 +47,16 @@ type Factory interface { // DeleteForwarder deletes a pod forwarder. DeleteForwarder(path string) - // Forwards returns all portforwards. + // Forwarders returns all portforwards. Forwarders() watch.Forwarders } +// ImageLister tracks resources with container images. +type ImageLister interface { + // ListImages lists container images. + ListImages(ctx context.Context, path string) ([]string, error) +} + // Getter represents a resource getter. type Getter interface { // Get return a given resource. @@ -92,7 +101,7 @@ type NodeMaintainer interface { // Loggable represents resources with logs. type Loggable interface { - // TaiLogs streams resource logs. + // TailLogs streams resource logs. TailLogs(ctx context.Context, opts *LogOptions) ([]LogChan, error) } @@ -149,10 +158,10 @@ type Logger interface { // ContainsPodSpec represents a resource with a pod template. type ContainsPodSpec interface { - // Get PodSpec of a resource + // GetPodSpec returns a podspec for the resource. GetPodSpec(path string) (*v1.PodSpec, error) - // Set Images for a resource + // SetImages sets container image. SetImages(ctx context.Context, path string, imageSpecs ImageSpecs) error } @@ -161,3 +170,9 @@ type Sanitizer interface { // Sanitize nukes all resources in unhappy state. Sanitize(context.Context, string) (int, error) } + +// Valuer represents a resource with values. +type Valuer interface { + // GetValues returns values for a resource. + GetValues(path string, allValues bool) ([]byte, error) +} diff --git a/internal/dao/utils_test.go b/internal/dao/utils_test.go new file mode 100644 index 0000000000..1984f63acb --- /dev/null +++ b/internal/dao/utils_test.go @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package dao_test + +import ( + "encoding/json" + "fmt" + "os" + "path" + "strings" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/watch" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/informers" +) + +type testFactory struct { + inventory map[string]map[string][]runtime.Object +} + +func makeFactory() dao.Factory { + return &testFactory{ + inventory: map[string]map[string][]runtime.Object{ + "kube-system": { + "v1/secrets": { + load("secret"), + }, + }, + }, + } +} + +var _ dao.Factory = &testFactory{} + +func (f *testFactory) Client() client.Connection { + return nil +} +func (f *testFactory) Get(gvr, fqn string, wait bool, sel labels.Selector) (runtime.Object, error) { + ns, po := path.Split(fqn) + ns = strings.Trim(ns, "/") + + for _, o := range f.inventory[ns][gvr] { + if o.(*unstructured.Unstructured).GetName() == po { + return o, nil + } + } + + return nil, nil +} +func (f *testFactory) List(gvr, ns string, wait bool, sel labels.Selector) ([]runtime.Object, error) { + return f.inventory[ns][gvr], nil +} + +func (f *testFactory) ForResource(ns, gvr string) (informers.GenericInformer, error) { + return nil, nil +} +func (f *testFactory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) { + return nil, nil +} +func (f *testFactory) WaitForCacheSync() {} +func (f *testFactory) Forwarders() watch.Forwarders { + return nil +} +func (f *testFactory) DeleteForwarder(string) {} + +func load(n string) *unstructured.Unstructured { + raw, _ := os.ReadFile(fmt.Sprintf("testdata/%s.json", n)) + + var o unstructured.Unstructured + _ = json.Unmarshal(raw, &o) + + return &o +} diff --git a/internal/dao/workload.go b/internal/dao/workload.go new file mode 100644 index 0000000000..604c6ca9ad --- /dev/null +++ b/internal/dao/workload.go @@ -0,0 +1,232 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package dao + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/rs/zerolog/log" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +const ( + StatusOK = "OK" + DegradedStatus = "DEGRADED" +) + +var ( + SaGVR = client.NewGVR("v1/serviceaccounts") + PvcGVR = client.NewGVR("v1/persistentvolumeclaims") + PcGVR = client.NewGVR("scheduling.k8s.io/v1/priorityclasses") + CmGVR = client.NewGVR("v1/configmaps") + SecGVR = client.NewGVR("v1/secrets") + PodGVR = client.NewGVR("v1/pods") + SvcGVR = client.NewGVR("v1/services") + DsGVR = client.NewGVR("apps/v1/daemonsets") + StsGVR = client.NewGVR("apps/v1/statefulSets") + DpGVR = client.NewGVR("apps/v1/deployments") + RsGVR = client.NewGVR("apps/v1/replicasets") + resList = []client.GVR{PodGVR, SvcGVR, DsGVR, StsGVR, DpGVR, RsGVR} +) + +// Workload tracks a select set of resources in a given namespace. +type Workload struct { + Table +} + +func (w *Workload) Delete(ctx context.Context, path string, propagation *metav1.DeletionPropagation, grace Grace) error { + gvr, _ := ctx.Value(internal.KeyGVR).(client.GVR) + ns, n := client.Namespaced(path) + auth, err := w.Client().CanI(ns, gvr.String(), n, []string{client.DeleteVerb}) + if err != nil { + return err + } + if !auth { + return fmt.Errorf("user is not authorized to delete %s", path) + } + + var gracePeriod *int64 + if grace != DefaultGrace { + gracePeriod = (*int64)(&grace) + } + opts := metav1.DeleteOptions{ + PropagationPolicy: propagation, + GracePeriodSeconds: gracePeriod, + } + + ctx, cancel := context.WithTimeout(ctx, w.Client().Config().CallTimeout()) + defer cancel() + + d, err := w.Client().DynDial() + if err != nil { + return err + } + dial := d.Resource(gvr.GVR()) + if client.IsClusterScoped(ns) { + return dial.Delete(ctx, n, opts) + } + + return dial.Namespace(ns).Delete(ctx, n, opts) +} + +func (a *Workload) fetch(ctx context.Context, gvr client.GVR, ns string) (*metav1.Table, error) { + a.Table.gvr = gvr + oo, err := a.Table.List(ctx, ns) + if err != nil { + return nil, err + } + if len(oo) == 0 { + return nil, fmt.Errorf("no table found for gvr: %s", gvr) + } + tt, ok := oo[0].(*metav1.Table) + if !ok { + return nil, errors.New("not a metav1.Table") + } + + return tt, nil +} + +// List fetch workloads. +func (a *Workload) List(ctx context.Context, ns string) ([]runtime.Object, error) { + oo := make([]runtime.Object, 0, 100) + for _, gvr := range resList { + table, err := a.fetch(ctx, gvr, ns) + if err != nil { + return nil, err + } + var ( + ns string + ts metav1.Time + ) + for _, r := range table.Rows { + if obj := r.Object.Object; obj != nil { + if m, err := meta.Accessor(obj); err == nil { + ns = m.GetNamespace() + ts = m.GetCreationTimestamp() + } + } else { + var m metav1.PartialObjectMetadata + if err := json.Unmarshal(r.Object.Raw, &m); err == nil { + ns = m.GetNamespace() + ts = m.CreationTimestamp + } + } + stat := status(gvr, r, table.ColumnDefinitions) + oo = append(oo, &render.WorkloadRes{Row: metav1.TableRow{Cells: []interface{}{ + gvr.String(), + ns, + r.Cells[indexOf("Name", table.ColumnDefinitions)], + stat, + readiness(gvr, r, table.ColumnDefinitions), + validity(stat), + ts, + }}}) + } + } + + return oo, nil +} + +// Helpers... + +func readiness(gvr client.GVR, r metav1.TableRow, h []metav1.TableColumnDefinition) string { + switch gvr { + case PodGVR, DpGVR, StsGVR: + return r.Cells[indexOf("Ready", h)].(string) + case RsGVR, DsGVR: + c := r.Cells[indexOf("Ready", h)].(int64) + d := r.Cells[indexOf("Desired", h)].(int64) + return fmt.Sprintf("%d/%d", c, d) + case SvcGVR: + return "" + } + + return render.NAValue +} + +func status(gvr client.GVR, r metav1.TableRow, h []metav1.TableColumnDefinition) string { + switch gvr { + case PodGVR: + if status := r.Cells[indexOf("Status", h)]; status == render.PhaseCompleted { + return StatusOK + } else if !isReady(r.Cells[indexOf("Ready", h)].(string)) || status != render.PhaseRunning { + return DegradedStatus + } + case DpGVR, StsGVR: + if !isReady(r.Cells[indexOf("Ready", h)].(string)) { + return DegradedStatus + } + case RsGVR, DsGVR: + rd, ok1 := r.Cells[indexOf("Ready", h)].(int64) + de, ok2 := r.Cells[indexOf("Desired", h)].(int64) + if ok1 && ok2 { + if !isReady(fmt.Sprintf("%d/%d", rd, de)) { + return DegradedStatus + } + break + } + rds, oks1 := r.Cells[indexOf("Ready", h)].(string) + des, oks2 := r.Cells[indexOf("Desired", h)].(string) + if oks1 && oks2 { + if !isReady(fmt.Sprintf("%s/%s", rds, des)) { + return DegradedStatus + } + } + case SvcGVR: + default: + return render.MissingValue + } + + return StatusOK +} + +func validity(status string) string { + if status != "DEGRADED" { + return "" + } + + return status +} + +func isReady(s string) bool { + tt := strings.Split(s, "/") + if len(tt) != 2 { + return false + } + r, err := strconv.Atoi(tt[0]) + if err != nil { + log.Error().Msgf("invalid ready count: %q", tt[0]) + return false + } + c, err := strconv.Atoi(tt[1]) + if err != nil { + log.Error().Msgf("invalid expected count: %q", tt[1]) + return false + } + + if c == 0 { + return true + } + return r == c +} + +func indexOf(n string, defs []metav1.TableColumnDefinition) int { + for i, d := range defs { + if d.Name == n { + return i + } + } + + return -1 +} diff --git a/internal/health/check.go b/internal/health/check.go index 14a56c1293..745c6e2af4 100644 --- a/internal/health/check.go +++ b/internal/health/check.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package health import ( diff --git a/internal/health/check_test.go b/internal/health/check_test.go index 0cdd27030e..1b6f112684 100644 --- a/internal/health/check_test.go +++ b/internal/health/check_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package health_test import ( diff --git a/internal/health/types.go b/internal/health/types.go index 4bb5de2117..b2c0822961 100644 --- a/internal/health/types.go +++ b/internal/health/types.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package health // Level tracks health count categories. diff --git a/internal/helpers.go b/internal/helpers.go new file mode 100644 index 0000000000..aa1d43e50f --- /dev/null +++ b/internal/helpers.go @@ -0,0 +1,46 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package internal + +import ( + "regexp" + "strings" + + "github.com/derailed/k9s/internal/view/cmd" +) + +var ( + inverseRx = regexp.MustCompile(`\A\!`) + fuzzyRx = regexp.MustCompile(`\A-f\s?([\w-]+)\b`) + labelRx = regexp.MustCompile(`\A\-l`) +) + +// Helpers... + +// IsInverseSelector checks if inverse char has been provided. +func IsInverseSelector(s string) bool { + if s == "" { + return false + } + return inverseRx.MatchString(s) +} + +// IsLabelSelector checks if query is a label query. +func IsLabelSelector(s string) bool { + if labelRx.MatchString(s) { + return true + } + + return !strings.Contains(s, " ") && cmd.ToLabels(s) != nil +} + +// IsFuzzySelector checks if query is fuzzy. +func IsFuzzySelector(s string) (string, bool) { + mm := fuzzyRx.FindStringSubmatch(s) + if len(mm) != 2 { + return "", false + } + + return mm[1], true +} diff --git a/internal/helpers_test.go b/internal/helpers_test.go new file mode 100644 index 0000000000..7ac6bc9985 --- /dev/null +++ b/internal/helpers_test.go @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package internal_test + +import ( + "testing" + + "github.com/derailed/k9s/internal" + "github.com/stretchr/testify/assert" +) + +func TestIsLabelSelector(t *testing.T) { + uu := map[string]struct { + s string + ok bool + }{ + "empty": {s: ""}, + "cool": {s: "-l app=fred,env=blee", ok: true}, + "no-flag": {s: "app=fred,env=blee", ok: true}, + "no-space": {s: "-lapp=fred,env=blee", ok: true}, + "wrong-flag": {s: "-f app=fred,env=blee"}, + "missing-key": {s: "=fred"}, + "missing-val": {s: "fred="}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.ok, internal.IsLabelSelector(u.s)) + }) + } +} diff --git a/internal/keys.go b/internal/keys.go index f184b49d5f..d18bc11d36 100644 --- a/internal/keys.go +++ b/internal/keys.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package internal // ContextKey represents context key. @@ -5,30 +8,32 @@ type ContextKey string // A collection of context keys. const ( - KeyFactory ContextKey = "factory" - KeyLabels ContextKey = "labels" - KeyFields ContextKey = "fields" - KeyTable ContextKey = "table" - KeyDir ContextKey = "dir" - KeyPath ContextKey = "path" - KeySubject ContextKey = "subject" - KeyGVR ContextKey = "gvr" - KeyForwards ContextKey = "forwards" - KeyContainers ContextKey = "containers" - KeyBenchCfg ContextKey = "benchcfg" - KeyAliases ContextKey = "aliases" - KeyUID ContextKey = "uid" - KeySubjectKind ContextKey = "subjectKind" - KeySubjectName ContextKey = "subjectName" - KeyNamespace ContextKey = "namespace" - KeyCluster ContextKey = "cluster" - KeyApp ContextKey = "app" - KeyStyles ContextKey = "styles" - KeyMetrics ContextKey = "metrics" - KeyHasMetrics ContextKey = "has-metrics" - KeyToast ContextKey = "toast" - KeyWithMetrics ContextKey = "withMetrics" - KeyViewConfig ContextKey = "viewConfig" - KeyWait ContextKey = "wait" - KeyPodCounting ContextKey = "podCounting" + KeyFactory ContextKey = "factory" + KeyLabels ContextKey = "labels" + KeyFields ContextKey = "fields" + KeyTable ContextKey = "table" + KeyDir ContextKey = "dir" + KeyPath ContextKey = "path" + KeySubject ContextKey = "subject" + KeyGVR ContextKey = "gvr" + KeyFQN ContextKey = "fqn" + KeyForwards ContextKey = "forwards" + KeyContainers ContextKey = "containers" + KeyBenchCfg ContextKey = "benchcfg" + KeyAliases ContextKey = "aliases" + KeyUID ContextKey = "uid" + KeySubjectKind ContextKey = "subjectKind" + KeySubjectName ContextKey = "subjectName" + KeyNamespace ContextKey = "namespace" + KeyCluster ContextKey = "cluster" + KeyApp ContextKey = "app" + KeyStyles ContextKey = "styles" + KeyMetrics ContextKey = "metrics" + KeyHasMetrics ContextKey = "has-metrics" + KeyToast ContextKey = "toast" + KeyWithMetrics ContextKey = "withMetrics" + KeyViewConfig ContextKey = "viewConfig" + KeyWait ContextKey = "wait" + KeyPodCounting ContextKey = "podCounting" + KeyEnableImgScan ContextKey = "vulScan" ) diff --git a/internal/model/cluster.go b/internal/model/cluster.go index 363c32325c..e598217e92 100644 --- a/internal/model/cluster.go +++ b/internal/model/cluster.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( @@ -55,7 +58,7 @@ func NewCluster(f dao.Factory) *Cluster { // Version returns the current K8s cluster version. func (c *Cluster) Version() string { info, err := c.factory.Client().ServerVersion() - if err != nil { + if err != nil || info == nil { return client.NA } @@ -71,7 +74,7 @@ func (c *Cluster) ContextName() string { return n } -// ClusterName returns the cluster name. +// ClusterName returns the context name. func (c *Cluster) ClusterName() string { n, err := c.factory.Client().Config().CurrentClusterName() if err != nil { @@ -105,7 +108,7 @@ func (c *Cluster) Metrics(ctx context.Context, mx *client.ClusterMetrics) error } } if nn == nil { - return errors.New("Unable to fetch nodes list") + return errors.New("unable to fetch nodes list") } if len(nn.Items) > 0 { c.cache.Add(clusterNodesKey, nn, clusterCacheExpiry) diff --git a/internal/model/cluster_info.go b/internal/model/cluster_info.go index 0f9d98c280..62f6e7f1dd 100644 --- a/internal/model/cluster_info.go +++ b/internal/model/cluster_info.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( @@ -6,8 +9,11 @@ import ( "errors" "io" "net/http" + "sync" "time" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/rs/zerolog/log" @@ -69,24 +75,25 @@ func (c ClusterMeta) Deltas(n ClusterMeta) bool { // ClusterInfo models cluster metadata. type ClusterInfo struct { - cluster *Cluster - factory dao.Factory - data ClusterMeta - version string - skipLatestRevCheck bool - listeners []ClusterInfoListener - cache *cache.LRUExpireCache + cluster *Cluster + factory dao.Factory + data ClusterMeta + version string + cfg *config.K9s + listeners []ClusterInfoListener + cache *cache.LRUExpireCache + mx sync.RWMutex } // NewClusterInfo returns a new instance. -func NewClusterInfo(f dao.Factory, v string, skipLatestRevCheck bool) *ClusterInfo { +func NewClusterInfo(f dao.Factory, v string, cfg *config.K9s) *ClusterInfo { c := ClusterInfo{ - factory: f, - cluster: NewCluster(f), - data: NewClusterMeta(), - version: v, - skipLatestRevCheck: skipLatestRevCheck, - cache: cache.NewLRUExpireCache(cacheSize), + factory: f, + cluster: NewCluster(f), + data: NewClusterMeta(), + version: v, + cfg: cfg, + cache: cache.NewLRUExpireCache(cacheSize), } return &c @@ -110,7 +117,16 @@ func (c *ClusterInfo) fetchK9sLatestRev() string { // Reset resets context and reload. func (c *ClusterInfo) Reset(f dao.Factory) { - c.cluster, c.data = NewCluster(f), NewClusterMeta() + if f == nil { + return + } + + c.mx.Lock() + { + c.cluster, c.data = NewCluster(f), NewClusterMeta() + } + c.mx.Unlock() + c.Refresh() } @@ -127,15 +143,13 @@ func (c *ClusterInfo) Refresh() { var mx client.ClusterMetrics if err := c.cluster.Metrics(ctx, &mx); err == nil { data.Cpu, data.Mem, data.Ephemeral = mx.PercCPU, mx.PercMEM, mx.PercEphemeral - } else { - log.Warn().Err(err).Msgf("Cluster metrics failed") } } data.K9sVer = c.version v1 := NewSemVer(data.K9sVer) var latestRev string - if !c.skipLatestRevCheck { + if !c.cfg.SkipLatestRevCheck { latestRev = c.fetchK9sLatestRev() } v2 := NewSemVer(latestRev) @@ -150,7 +164,11 @@ func (c *ClusterInfo) Refresh() { } else { c.fireNoMetaChanged(data) } - c.data = data + c.mx.Lock() + { + c.data = data + } + c.mx.Unlock() } // AddListener adds a new model listener. diff --git a/internal/model/cluster_info_test.go b/internal/model/cluster_info_test.go index 84155a651f..48426ba5e2 100644 --- a/internal/model/cluster_info_test.go +++ b/internal/model/cluster_info_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model_test import ( diff --git a/internal/model/cmd_buff.go b/internal/model/cmd_buff.go index ba4db4b392..f62ca79949 100644 --- a/internal/model/cmd_buff.go +++ b/internal/model/cmd_buff.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( diff --git a/internal/model/cmd_buff_test.go b/internal/model/cmd_buff_test.go index 0933affbca..924470ad3e 100644 --- a/internal/model/cmd_buff_test.go +++ b/internal/model/cmd_buff_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model_test import ( diff --git a/internal/model/describe.go b/internal/model/describe.go index 8a44af1bd9..2d5d845a62 100644 --- a/internal/model/describe.go +++ b/internal/model/describe.go @@ -1,15 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( "context" "fmt" "reflect" - "regexp" "strings" "sync/atomic" "time" backoff "github.com/cenkalti/backoff/v4" + "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/rs/zerolog/log" @@ -25,6 +28,7 @@ type Describe struct { lines []string refreshRate time.Duration listeners []ResourceViewerListener + decode bool } // NewDescribe returns a new describe resource model. @@ -36,6 +40,11 @@ func NewDescribe(gvr client.GVR, path string) *Describe { } } +// GVR returns the resource gvr. +func (d *Describe) GVR() client.GVR { + return d.gvr +} + // GetPath returns the active resource path. func (d *Describe) GetPath() string { return d.path @@ -58,31 +67,16 @@ func (d *Describe) filter(q string, lines []string) fuzzy.Matches { if q == "" { return nil } - if dao.IsFuzzySelector(q) { - return d.fuzzyFilter(strings.TrimSpace(q[2:]), lines) + if f, ok := internal.IsFuzzySelector(q); ok { + return d.fuzzyFilter(strings.TrimSpace(f), lines) } - return d.rxFilter(q, lines) + return rxFilter(q, lines) } func (*Describe) fuzzyFilter(q string, lines []string) fuzzy.Matches { return fuzzy.Find(q, lines) } -func (*Describe) rxFilter(q string, lines []string) fuzzy.Matches { - rx, err := regexp.Compile(`(?i)` + q) - if err != nil { - return nil - } - matches := make(fuzzy.Matches, 0, len(lines)) - for i, l := range lines { - if loc := rx.FindStringIndex(l); len(loc) == 2 { - matches = append(matches, fuzzy.Match{Str: q, Index: i, MatchedIndexes: loc}) - } - } - - return matches -} - func (d *Describe) fireResourceChanged(lines []string, matches fuzzy.Matches) { for _, l := range d.listeners { l.ResourceChanged(lines, matches) @@ -188,6 +182,10 @@ func (d *Describe) describe(ctx context.Context, gvr client.GVR, path string) (s return "", fmt.Errorf("no describer for %q", meta.DAO.GVR()) } + if desc, ok := meta.DAO.(*dao.Secret); ok { + desc.SetDecode(d.decode) + } + return desc.Describe(path) } @@ -210,3 +208,8 @@ func (d *Describe) RemoveListener(l ResourceViewerListener) { d.listeners = append(d.listeners[:victim], d.listeners[victim+1:]...) } } + +// Toggle toggles the decode flag. +func (d *Describe) Toggle() { + d.decode = !d.decode +} diff --git a/internal/model/fish_buff.go b/internal/model/fish_buff.go index 77098df946..1bfb0ae003 100644 --- a/internal/model/fish_buff.go +++ b/internal/model/fish_buff.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( @@ -133,5 +136,4 @@ func (f *FishBuff) fireSuggestionChanged(ss []string) { suggest = ss[f.suggestionIndex] } f.SetText(f.GetText(), suggest) - } diff --git a/internal/model/fish_buff_test.go b/internal/model/fish_buff_test.go index 7fcde49866..ae8abe3ca2 100644 --- a/internal/model/fish_buff_test.go +++ b/internal/model/fish_buff_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model_test import ( diff --git a/internal/model/flash.go b/internal/model/flash.go index 150ae8bce6..def08b090e 100644 --- a/internal/model/flash.go +++ b/internal/model/flash.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( diff --git a/internal/model/flash_test.go b/internal/model/flash_test.go index 2749239f2a..2484fdef58 100644 --- a/internal/model/flash_test.go +++ b/internal/model/flash_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model_test import ( diff --git a/internal/model/helpers.go b/internal/model/helpers.go index c09cd5e5e5..2173149472 100644 --- a/internal/model/helpers.go +++ b/internal/model/helpers.go @@ -1,12 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( "context" + "regexp" "time" "github.com/cenkalti/backoff/v4" - "github.com/derailed/tview" - runewidth "github.com/mattn/go-runewidth" + "github.com/sahilm/fuzzy" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -23,14 +26,31 @@ func FQN(ns, n string) string { return ns + "/" + n } -// Truncate a string to the given l and suffix ellipsis if needed. -func Truncate(str string, width int) string { - return runewidth.Truncate(str, width, string(tview.SemigraphicsHorizontalEllipsis)) -} - // NewExpBackOff returns a new exponential backoff timer. func NewExpBackOff(ctx context.Context, start, max time.Duration) backoff.BackOffContext { bf := backoff.NewExponentialBackOff() bf.InitialInterval, bf.MaxElapsedTime = start, max return backoff.WithContext(bf, ctx) } + +func rxFilter(q string, lines []string) fuzzy.Matches { + rx, err := regexp.Compile(`(?i)` + q) + if err != nil { + return nil + } + + matches := make(fuzzy.Matches, 0, len(lines)) + for i, l := range lines { + locs := rx.FindAllStringIndex(l, -1) + for _, loc := range locs { + indexes := make([]int, 0, loc[1]-loc[0]) + for v := loc[0]; v < loc[1]; v++ { + indexes = append(indexes, v) + } + + matches = append(matches, fuzzy.Match{Str: q, Index: i, MatchedIndexes: indexes}) + } + } + + return matches +} diff --git a/internal/model/yaml_test.go b/internal/model/helpers_int_test.go similarity index 75% rename from internal/model/yaml_test.go rename to internal/model/helpers_int_test.go index 9b595134e0..5c3f39a911 100644 --- a/internal/model/yaml_test.go +++ b/internal/model/helpers_int_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( @@ -7,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestYAML_rxFilter(t *testing.T) { +func Test_rxFilter(t *testing.T) { uu := map[string]struct { q string lines []string @@ -29,7 +32,7 @@ func TestYAML_rxFilter(t *testing.T) { { Str: "foo", Index: 0, - MatchedIndexes: []int{0, 3}, + MatchedIndexes: []int{0, 1, 2}, }, }, }, @@ -40,26 +43,25 @@ func TestYAML_rxFilter(t *testing.T) { { Str: "foo", Index: 0, - MatchedIndexes: []int{0, 3}, + MatchedIndexes: []int{0, 1, 2}, }, { Str: "foo", Index: 2, - MatchedIndexes: []int{0, 3}, + MatchedIndexes: []int{0, 1, 2}, }, { Str: "foo", Index: 2, - MatchedIndexes: []int{8, 11}, + MatchedIndexes: []int{8, 9, 10}, }, }, }, } - var y YAML for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, y.rxFilter(u.q, u.lines)) + assert.Equal(t, u.e, rxFilter(u.q, u.lines)) }) } } diff --git a/internal/model/helpers_test.go b/internal/model/helpers_test.go index ce45d5ecd9..69ed2529e5 100644 --- a/internal/model/helpers_test.go +++ b/internal/model/helpers_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model_test import ( @@ -30,34 +33,3 @@ func TestMetaFQN(t *testing.T) { }) } } - -func TestTruncate(t *testing.T) { - uu := map[string]struct { - data string - size int - e string - }{ - "same": { - data: "fred", - size: 4, - e: "fred", - }, - "small": { - data: "fred", - size: 10, - e: "fred", - }, - "larger": { - data: "fred", - size: 3, - e: "fr…", - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, model.Truncate(u.data, u.size)) - }) - } -} diff --git a/internal/model/hint.go b/internal/model/hint.go index 84a5ecdd49..8bdeb850e2 100644 --- a/internal/model/hint.go +++ b/internal/model/hint.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model // HintListener represents a menu hints listener. diff --git a/internal/model/hint_test.go b/internal/model/hint_test.go index ef1af0966f..383b2c5096 100644 --- a/internal/model/hint_test.go +++ b/internal/model/hint_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model_test import ( diff --git a/internal/model/history.go b/internal/model/history.go index 922720f3f2..7469e32af6 100644 --- a/internal/model/history.go +++ b/internal/model/history.go @@ -1,9 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( "strings" - - "github.com/rs/zerolog/log" ) // MaxHistory tracks max command history. @@ -22,6 +23,14 @@ func NewHistory(limit int) *History { } } +func (h *History) Pop() string { + if h.Empty() { + return "" + } + + return h.commands[0] +} + // List returns the current command history. func (h *History) List() []string { return h.commands @@ -46,7 +55,6 @@ func (h *History) Push(c string) { // Clear clears out the stack. func (h *History) Clear() { - log.Debug().Msgf("History CLEARED!!!") h.commands = nil } diff --git a/internal/model/history_test.go b/internal/model/history_test.go index d4fade171d..1655675228 100644 --- a/internal/model/history_test.go +++ b/internal/model/history_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model_test import ( diff --git a/internal/model/log.go b/internal/model/log.go index d0e7d88438..43f294c3c3 100644 --- a/internal/model/log.go +++ b/internal/model/log.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( @@ -111,7 +114,7 @@ func (l *Log) SetSinceSeconds(ctx context.Context, i int64) { } // Configure sets logger configuration. -func (l *Log) Configure(opts *config.Logger) { +func (l *Log) Configure(opts config.Logger) { l.logOptions.Lines = int64(opts.TailCount) l.logOptions.SinceSeconds = opts.SinceSeconds } diff --git a/internal/model/log_int_test.go b/internal/model/log_int_test.go index c7d17c659b..57dd952078 100644 --- a/internal/model/log_int_test.go +++ b/internal/model/log_int_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( diff --git a/internal/model/log_test.go b/internal/model/log_test.go index a0432fb0f0..8f09f86ea4 100644 --- a/internal/model/log_test.go +++ b/internal/model/log_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model_test import ( diff --git a/internal/model/menu_hint.go b/internal/model/menu_hint.go index 7d1da43e7e..2ac0ffc9e4 100644 --- a/internal/model/menu_hint.go +++ b/internal/model/menu_hint.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( diff --git a/internal/model/menu_hint_test.go b/internal/model/menu_hint_test.go index d364dc4c60..0aa23449be 100644 --- a/internal/model/menu_hint_test.go +++ b/internal/model/menu_hint_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model_test import ( diff --git a/internal/model/mock_clustermeta_test.go b/internal/model/mock_clustermeta_test.go deleted file mode 100644 index a47a8434b6..0000000000 --- a/internal/model/mock_clustermeta_test.go +++ /dev/null @@ -1,866 +0,0 @@ -// Code generated by pegomock. DO NOT EDIT. -// Source: github.com/derailed/k9s/internal/model (interfaces: ClusterMeta) - -package model_test - -import ( - "reflect" - "time" - - client "github.com/derailed/k9s/internal/client" - pegomock "github.com/petergtz/pegomock" - v1 "k8s.io/api/core/v1" - version "k8s.io/apimachinery/pkg/version" - disk "k8s.io/client-go/discovery/cached/disk" - dynamic "k8s.io/client-go/dynamic" - kubernetes "k8s.io/client-go/kubernetes" - rest "k8s.io/client-go/rest" - versioned "k8s.io/metrics/pkg/client/clientset/versioned" -) - -type MockClusterMeta struct { - fail func(message string, callerSkip ...int) -} - -func NewMockClusterMeta(options ...pegomock.Option) *MockClusterMeta { - mock := &MockClusterMeta{} - for _, option := range options { - option.Apply(mock) - } - return mock -} - -func (mock *MockClusterMeta) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } -func (mock *MockClusterMeta) FailHandler() pegomock.FailHandler { return mock.fail } - -func (mock *MockClusterMeta) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CachedDiscovery", params, []reflect.Type{reflect.TypeOf((**disk.CachedDiscoveryClient)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *disk.CachedDiscoveryClient - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*disk.CachedDiscoveryClient) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) CanI(_param0 string, _param1 string, _param2 []string) (bool, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{_param0, _param1, _param2} - result := pegomock.GetGenericMockFrom(mock).Invoke("CanI", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 bool - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) ClusterName() (string, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ClusterName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) Config() *client.Config { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("Config", params, []reflect.Type{reflect.TypeOf((**client.Config)(nil)).Elem()}) - var ret0 *client.Config - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*client.Config) - } - } - return ret0 -} - -func (mock *MockClusterMeta) ContextName() (string, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ContextName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) CurrentNamespaceName() (string, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CurrentNamespaceName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) DialOrDie() kubernetes.Interface { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("DialOrDie", params, []reflect.Type{reflect.TypeOf((*kubernetes.Interface)(nil)).Elem()}) - var ret0 kubernetes.Interface - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(kubernetes.Interface) - } - } - return ret0 -} - -func (mock *MockClusterMeta) DynDialOrDie() dynamic.Interface { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("DynDialOrDie", params, []reflect.Type{reflect.TypeOf((*dynamic.Interface)(nil)).Elem()}) - var ret0 dynamic.Interface - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(dynamic.Interface) - } - } - return ret0 -} - -func (mock *MockClusterMeta) GetNodes() (*v1.NodeList, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("GetNodes", params, []reflect.Type{reflect.TypeOf((**v1.NodeList)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *v1.NodeList - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*v1.NodeList) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) HasMetrics() bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("HasMetrics", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockClusterMeta) IsNamespaced(_param0 string) bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("IsNamespaced", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockClusterMeta) MXDial() (*versioned.Clientset, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("MXDial", params, []reflect.Type{reflect.TypeOf((**versioned.Clientset)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *versioned.Clientset - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*versioned.Clientset) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) NodePods(_param0 string) (*v1.PodList, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("NodePods", params, []reflect.Type{reflect.TypeOf((**v1.PodList)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *v1.PodList - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*v1.PodList) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) RestConfigOrDie() *rest.Config { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("RestConfigOrDie", params, []reflect.Type{reflect.TypeOf((**rest.Config)(nil)).Elem()}) - var ret0 *rest.Config - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*rest.Config) - } - } - return ret0 -} - -func (mock *MockClusterMeta) ServerVersion() (*version.Info, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ServerVersion", params, []reflect.Type{reflect.TypeOf((**version.Info)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *version.Info - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*version.Info) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) SupportsRes(_param0 string, _param1 []string) (string, bool, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{_param0, _param1} - result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsRes", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 bool - var ret2 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(bool) - } - if result[2] != nil { - ret2 = result[2].(error) - } - } - return ret0, ret1, ret2 -} - -func (mock *MockClusterMeta) SupportsResource(_param0 string) bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("SupportsResource", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockClusterMeta) SwitchContext(_param0 string) error { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{_param0} - pegomock.GetGenericMockFrom(mock).Invoke("SwitchContext", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) - - return nil -} - -func (mock *MockClusterMeta) UserName() (string, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("UserName", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) ValidNamespaces() ([]v1.Namespace, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ValidNamespaces", params, []reflect.Type{reflect.TypeOf((*[]v1.Namespace)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 []v1.Namespace - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].([]v1.Namespace) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) Version() (string, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockClusterMeta().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("Version", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 string - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockClusterMeta) VerifyWasCalledOnce() *VerifierMockClusterMeta { - return &VerifierMockClusterMeta{ - mock: mock, - invocationCountMatcher: pegomock.Times(1), - } -} - -func (mock *MockClusterMeta) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockClusterMeta { - return &VerifierMockClusterMeta{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - } -} - -func (mock *MockClusterMeta) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockClusterMeta { - return &VerifierMockClusterMeta{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - inOrderContext: inOrderContext, - } -} - -func (mock *MockClusterMeta) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockClusterMeta { - return &VerifierMockClusterMeta{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - timeout: timeout, - } -} - -type VerifierMockClusterMeta struct { - mock *MockClusterMeta - invocationCountMatcher pegomock.Matcher - inOrderContext *pegomock.InOrderContext - timeout time.Duration -} - -func (verifier *VerifierMockClusterMeta) CachedDiscovery() *MockClusterMeta_CachedDiscovery_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CachedDiscovery", params, verifier.timeout) - return &MockClusterMeta_CachedDiscovery_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_CachedDiscovery_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_CachedDiscovery_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_CachedDiscovery_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) CanI(_param0 string, _param1 string, _param2 []string) *MockClusterMeta_CanI_OngoingVerification { - params := []pegomock.Param{_param0, _param1, _param2} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CanI", params, verifier.timeout) - return &MockClusterMeta_CanI_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_CanI_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_CanI_OngoingVerification) GetCapturedArguments() (string, string, []string) { - _param0, _param1, _param2 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1], _param2[len(_param2)-1] -} - -func (c *MockClusterMeta_CanI_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 [][]string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - _param1 = make([]string, len(params[1])) - for u, param := range params[1] { - _param1[u] = param.(string) - } - _param2 = make([][]string, len(params[2])) - for u, param := range params[2] { - _param2[u] = param.([]string) - } - } - return -} - -func (verifier *VerifierMockClusterMeta) ClusterName() *MockClusterMeta_ClusterName_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ClusterName", params, verifier.timeout) - return &MockClusterMeta_ClusterName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_ClusterName_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_ClusterName_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_ClusterName_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) Config() *MockClusterMeta_Config_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Config", params, verifier.timeout) - return &MockClusterMeta_Config_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_Config_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_Config_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_Config_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) ContextName() *MockClusterMeta_ContextName_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ContextName", params, verifier.timeout) - return &MockClusterMeta_ContextName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_ContextName_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_ContextName_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_ContextName_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) CurrentNamespaceName() *MockClusterMeta_CurrentNamespaceName_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CurrentNamespaceName", params, verifier.timeout) - return &MockClusterMeta_CurrentNamespaceName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_CurrentNamespaceName_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_CurrentNamespaceName_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_CurrentNamespaceName_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) DialOrDie() *MockClusterMeta_DialOrDie_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DialOrDie", params, verifier.timeout) - return &MockClusterMeta_DialOrDie_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_DialOrDie_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_DialOrDie_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_DialOrDie_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) DynDialOrDie() *MockClusterMeta_DynDialOrDie_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DynDialOrDie", params, verifier.timeout) - return &MockClusterMeta_DynDialOrDie_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_DynDialOrDie_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_DynDialOrDie_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_DynDialOrDie_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) GetNodes() *MockClusterMeta_GetNodes_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetNodes", params, verifier.timeout) - return &MockClusterMeta_GetNodes_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_GetNodes_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_GetNodes_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_GetNodes_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) HasMetrics() *MockClusterMeta_HasMetrics_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasMetrics", params, verifier.timeout) - return &MockClusterMeta_HasMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_HasMetrics_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_HasMetrics_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_HasMetrics_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) IsNamespaced(_param0 string) *MockClusterMeta_IsNamespaced_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "IsNamespaced", params, verifier.timeout) - return &MockClusterMeta_IsNamespaced_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_IsNamespaced_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_IsNamespaced_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockClusterMeta_IsNamespaced_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockClusterMeta) MXDial() *MockClusterMeta_MXDial_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "MXDial", params, verifier.timeout) - return &MockClusterMeta_MXDial_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_MXDial_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_MXDial_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_MXDial_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) NodePods(_param0 string) *MockClusterMeta_NodePods_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "NodePods", params, verifier.timeout) - return &MockClusterMeta_NodePods_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_NodePods_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_NodePods_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockClusterMeta_NodePods_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockClusterMeta) RestConfigOrDie() *MockClusterMeta_RestConfigOrDie_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RestConfigOrDie", params, verifier.timeout) - return &MockClusterMeta_RestConfigOrDie_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_RestConfigOrDie_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_RestConfigOrDie_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_RestConfigOrDie_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) ServerVersion() *MockClusterMeta_ServerVersion_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ServerVersion", params, verifier.timeout) - return &MockClusterMeta_ServerVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_ServerVersion_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_ServerVersion_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_ServerVersion_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) SupportsRes(_param0 string, _param1 []string) *MockClusterMeta_SupportsRes_OngoingVerification { - params := []pegomock.Param{_param0, _param1} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SupportsRes", params, verifier.timeout) - return &MockClusterMeta_SupportsRes_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_SupportsRes_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_SupportsRes_OngoingVerification) GetCapturedArguments() (string, []string) { - _param0, _param1 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1] -} - -func (c *MockClusterMeta_SupportsRes_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 [][]string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - _param1 = make([][]string, len(params[1])) - for u, param := range params[1] { - _param1[u] = param.([]string) - } - } - return -} - -func (verifier *VerifierMockClusterMeta) SupportsResource(_param0 string) *MockClusterMeta_SupportsResource_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SupportsResource", params, verifier.timeout) - return &MockClusterMeta_SupportsResource_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_SupportsResource_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_SupportsResource_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockClusterMeta_SupportsResource_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockClusterMeta) SwitchContextOrDie(_param0 string) *MockClusterMeta_SwitchContextOrDie_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SwitchContextOrDie", params, verifier.timeout) - return &MockClusterMeta_SwitchContextOrDie_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_SwitchContextOrDie_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_SwitchContextOrDie_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockClusterMeta_SwitchContextOrDie_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockClusterMeta) UserName() *MockClusterMeta_UserName_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "UserName", params, verifier.timeout) - return &MockClusterMeta_UserName_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_UserName_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_UserName_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_UserName_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) ValidNamespaces() *MockClusterMeta_ValidNamespaces_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ValidNamespaces", params, verifier.timeout) - return &MockClusterMeta_ValidNamespaces_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_ValidNamespaces_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_ValidNamespaces_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_ValidNamespaces_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockClusterMeta) Version() *MockClusterMeta_Version_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Version", params, verifier.timeout) - return &MockClusterMeta_Version_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockClusterMeta_Version_OngoingVerification struct { - mock *MockClusterMeta - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockClusterMeta_Version_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockClusterMeta_Version_OngoingVerification) GetAllCapturedArguments() { -} diff --git a/internal/model/mock_connection_test.go b/internal/model/mock_connection_test.go deleted file mode 100644 index 9ce08a7ea8..0000000000 --- a/internal/model/mock_connection_test.go +++ /dev/null @@ -1,652 +0,0 @@ -// Code generated by pegomock. DO NOT EDIT. -// Source: github.com/derailed/k9s/internal/client (interfaces: Connection) - -package model_test - -import ( - client "github.com/derailed/k9s/internal/client" - pegomock "github.com/petergtz/pegomock" - v1 "k8s.io/api/core/v1" - version "k8s.io/apimachinery/pkg/version" - disk "k8s.io/client-go/discovery/cached/disk" - dynamic "k8s.io/client-go/dynamic" - kubernetes "k8s.io/client-go/kubernetes" - rest "k8s.io/client-go/rest" - versioned "k8s.io/metrics/pkg/client/clientset/versioned" - "reflect" - "time" -) - -type MockConnection struct { - fail func(message string, callerSkip ...int) -} - -func NewMockConnection(options ...pegomock.Option) *MockConnection { - mock := &MockConnection{} - for _, option := range options { - option.Apply(mock) - } - return mock -} - -func (mock *MockConnection) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } -func (mock *MockConnection) FailHandler() pegomock.FailHandler { return mock.fail } - -func (mock *MockConnection) ActiveCluster() string { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ActiveCluster", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()}) - var ret0 string - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - } - return ret0 -} - -func (mock *MockConnection) ActiveNamespace() string { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ActiveNamespace", params, []reflect.Type{reflect.TypeOf((*string)(nil)).Elem()}) - var ret0 string - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(string) - } - } - return ret0 -} - -func (mock *MockConnection) CachedDiscovery() (*disk.CachedDiscoveryClient, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CachedDiscovery", params, []reflect.Type{reflect.TypeOf((**disk.CachedDiscoveryClient)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *disk.CachedDiscoveryClient - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*disk.CachedDiscoveryClient) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) CanI(_param0 string, _param1 string, _param2 []string) (bool, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0, _param1, _param2} - result := pegomock.GetGenericMockFrom(mock).Invoke("CanI", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 bool - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) CheckConnectivity() bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("CheckConnectivity", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockConnection) Config() *client.Config { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("Config", params, []reflect.Type{reflect.TypeOf((**client.Config)(nil)).Elem()}) - var ret0 *client.Config - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*client.Config) - } - } - return ret0 -} - -func (mock *MockConnection) ConnectionOK() bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ConnectionOK", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockConnection) Dial() (kubernetes.Interface, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("Dial", params, []reflect.Type{reflect.TypeOf((*kubernetes.Interface)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 kubernetes.Interface - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(kubernetes.Interface) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) DynDial() (dynamic.Interface, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("DynDial", params, []reflect.Type{reflect.TypeOf((*dynamic.Interface)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 dynamic.Interface - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(dynamic.Interface) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) HasMetrics() bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("HasMetrics", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockConnection) IsActiveNamespace(_param0 string) bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("IsActiveNamespace", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockConnection) MXDial() (*versioned.Clientset, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("MXDial", params, []reflect.Type{reflect.TypeOf((**versioned.Clientset)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *versioned.Clientset - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*versioned.Clientset) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) RestConfig() (*rest.Config, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("RestConfig", params, []reflect.Type{reflect.TypeOf((**rest.Config)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *rest.Config - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*rest.Config) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) ServerVersion() (*version.Info, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ServerVersion", params, []reflect.Type{reflect.TypeOf((**version.Info)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *version.Info - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*version.Info) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) SwitchContext(_param0 string) error { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("SwitchContext", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(error) - } - } - return ret0 -} - -func (mock *MockConnection) ValidNamespaces() ([]v1.Namespace, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockConnection().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("ValidNamespaces", params, []reflect.Type{reflect.TypeOf((*[]v1.Namespace)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 []v1.Namespace - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].([]v1.Namespace) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockConnection) VerifyWasCalledOnce() *VerifierMockConnection { - return &VerifierMockConnection{ - mock: mock, - invocationCountMatcher: pegomock.Times(1), - } -} - -func (mock *MockConnection) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockConnection { - return &VerifierMockConnection{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - } -} - -func (mock *MockConnection) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockConnection { - return &VerifierMockConnection{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - inOrderContext: inOrderContext, - } -} - -func (mock *MockConnection) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockConnection { - return &VerifierMockConnection{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - timeout: timeout, - } -} - -type VerifierMockConnection struct { - mock *MockConnection - invocationCountMatcher pegomock.Matcher - inOrderContext *pegomock.InOrderContext - timeout time.Duration -} - -func (verifier *VerifierMockConnection) ActiveCluster() *MockConnection_ActiveCluster_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ActiveCluster", params, verifier.timeout) - return &MockConnection_ActiveCluster_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ActiveCluster_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ActiveCluster_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ActiveCluster_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) ActiveNamespace() *MockConnection_ActiveNamespace_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ActiveNamespace", params, verifier.timeout) - return &MockConnection_ActiveNamespace_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ActiveNamespace_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ActiveNamespace_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ActiveNamespace_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) CachedDiscovery() *MockConnection_CachedDiscovery_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CachedDiscovery", params, verifier.timeout) - return &MockConnection_CachedDiscovery_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CachedDiscovery_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CachedDiscovery_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_CachedDiscovery_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) CanI(_param0 string, _param1 string, _param2 []string) *MockConnection_CanI_OngoingVerification { - params := []pegomock.Param{_param0, _param1, _param2} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CanI", params, verifier.timeout) - return &MockConnection_CanI_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CanI_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CanI_OngoingVerification) GetCapturedArguments() (string, string, []string) { - _param0, _param1, _param2 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1], _param2[len(_param2)-1] -} - -func (c *MockConnection_CanI_OngoingVerification) GetAllCapturedArguments() (_param0 []string, _param1 []string, _param2 [][]string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(string) - } - _param1 = make([]string, len(c.methodInvocations)) - for u, param := range params[1] { - _param1[u] = param.(string) - } - _param2 = make([][]string, len(c.methodInvocations)) - for u, param := range params[2] { - _param2[u] = param.([]string) - } - } - return -} - -func (verifier *VerifierMockConnection) CheckConnectivity() *MockConnection_CheckConnectivity_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "CheckConnectivity", params, verifier.timeout) - return &MockConnection_CheckConnectivity_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_CheckConnectivity_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_CheckConnectivity_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_CheckConnectivity_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) Config() *MockConnection_Config_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Config", params, verifier.timeout) - return &MockConnection_Config_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_Config_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_Config_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_Config_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) ConnectionOK() *MockConnection_ConnectionOK_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ConnectionOK", params, verifier.timeout) - return &MockConnection_ConnectionOK_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ConnectionOK_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ConnectionOK_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ConnectionOK_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) Dial() *MockConnection_Dial_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "Dial", params, verifier.timeout) - return &MockConnection_Dial_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_Dial_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_Dial_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_Dial_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) DynDial() *MockConnection_DynDial_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "DynDial", params, verifier.timeout) - return &MockConnection_DynDial_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_DynDial_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_DynDial_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_DynDial_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) HasMetrics() *MockConnection_HasMetrics_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasMetrics", params, verifier.timeout) - return &MockConnection_HasMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_HasMetrics_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_HasMetrics_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_HasMetrics_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) IsActiveNamespace(_param0 string) *MockConnection_IsActiveNamespace_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "IsActiveNamespace", params, verifier.timeout) - return &MockConnection_IsActiveNamespace_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_IsActiveNamespace_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_IsActiveNamespace_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockConnection_IsActiveNamespace_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockConnection) MXDial() *MockConnection_MXDial_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "MXDial", params, verifier.timeout) - return &MockConnection_MXDial_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_MXDial_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_MXDial_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_MXDial_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) RestConfig() *MockConnection_RestConfig_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "RestConfig", params, verifier.timeout) - return &MockConnection_RestConfig_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_RestConfig_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_RestConfig_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_RestConfig_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) ServerVersion() *MockConnection_ServerVersion_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ServerVersion", params, verifier.timeout) - return &MockConnection_ServerVersion_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ServerVersion_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ServerVersion_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ServerVersion_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockConnection) SwitchContext(_param0 string) *MockConnection_SwitchContext_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "SwitchContext", params, verifier.timeout) - return &MockConnection_SwitchContext_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_SwitchContext_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_SwitchContext_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockConnection_SwitchContext_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(c.methodInvocations)) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockConnection) ValidNamespaces() *MockConnection_ValidNamespaces_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ValidNamespaces", params, verifier.timeout) - return &MockConnection_ValidNamespaces_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockConnection_ValidNamespaces_OngoingVerification struct { - mock *MockConnection - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockConnection_ValidNamespaces_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockConnection_ValidNamespaces_OngoingVerification) GetAllCapturedArguments() { -} diff --git a/internal/model/mock_metricsserver_test.go b/internal/model/mock_metricsserver_test.go deleted file mode 100644 index 8447f578a6..0000000000 --- a/internal/model/mock_metricsserver_test.go +++ /dev/null @@ -1,311 +0,0 @@ -// Code generated by pegomock. DO NOT EDIT. -// Source: github.com/derailed/k9s/internal/model (interfaces: MetricsServer) - -package model_test - -import ( - client "github.com/derailed/k9s/internal/client" - pegomock "github.com/petergtz/pegomock" - v1 "k8s.io/api/core/v1" - v1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" - "reflect" - "time" -) - -type MockMetricsServer struct { - fail func(message string, callerSkip ...int) -} - -func NewMockMetricsServer(options ...pegomock.Option) *MockMetricsServer { - mock := &MockMetricsServer{} - for _, option := range options { - option.Apply(mock) - } - return mock -} - -func (mock *MockMetricsServer) SetFailHandler(fh pegomock.FailHandler) { mock.fail = fh } -func (mock *MockMetricsServer) FailHandler() pegomock.FailHandler { return mock.fail } - -func (mock *MockMetricsServer) ClusterLoad(_param0 *v1.NodeList, _param1 *v1beta1.NodeMetricsList, _param2 *client.ClusterMetrics) error { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockMetricsServer().") - } - params := []pegomock.Param{_param0, _param1, _param2} - result := pegomock.GetGenericMockFrom(mock).Invoke("ClusterLoad", params, []reflect.Type{reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(error) - } - } - return ret0 -} - -func (mock *MockMetricsServer) FetchNodesMetrics() (*v1beta1.NodeMetricsList, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockMetricsServer().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("FetchNodesMetrics", params, []reflect.Type{reflect.TypeOf((**v1beta1.NodeMetricsList)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *v1beta1.NodeMetricsList - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*v1beta1.NodeMetricsList) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockMetricsServer) FetchPodsMetrics(_param0 string) (*v1beta1.PodMetricsList, error) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockMetricsServer().") - } - params := []pegomock.Param{_param0} - result := pegomock.GetGenericMockFrom(mock).Invoke("FetchPodsMetrics", params, []reflect.Type{reflect.TypeOf((**v1beta1.PodMetricsList)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) - var ret0 *v1beta1.PodMetricsList - var ret1 error - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(*v1beta1.PodMetricsList) - } - if result[1] != nil { - ret1 = result[1].(error) - } - } - return ret0, ret1 -} - -func (mock *MockMetricsServer) HasMetrics() bool { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockMetricsServer().") - } - params := []pegomock.Param{} - result := pegomock.GetGenericMockFrom(mock).Invoke("HasMetrics", params, []reflect.Type{reflect.TypeOf((*bool)(nil)).Elem()}) - var ret0 bool - if len(result) != 0 { - if result[0] != nil { - ret0 = result[0].(bool) - } - } - return ret0 -} - -func (mock *MockMetricsServer) NodesMetrics(_param0 *v1.NodeList, _param1 *v1beta1.NodeMetricsList, _param2 client.NodesMetrics) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockMetricsServer().") - } - params := []pegomock.Param{_param0, _param1, _param2} - pegomock.GetGenericMockFrom(mock).Invoke("NodesMetrics", params, []reflect.Type{}) -} - -func (mock *MockMetricsServer) PodsMetrics(_param0 *v1beta1.PodMetricsList, _param1 client.PodsMetrics) { - if mock == nil { - panic("mock must not be nil. Use myMock := NewMockMetricsServer().") - } - params := []pegomock.Param{_param0, _param1} - pegomock.GetGenericMockFrom(mock).Invoke("PodsMetrics", params, []reflect.Type{}) -} - -func (mock *MockMetricsServer) VerifyWasCalledOnce() *VerifierMockMetricsServer { - return &VerifierMockMetricsServer{ - mock: mock, - invocationCountMatcher: pegomock.Times(1), - } -} - -func (mock *MockMetricsServer) VerifyWasCalled(invocationCountMatcher pegomock.Matcher) *VerifierMockMetricsServer { - return &VerifierMockMetricsServer{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - } -} - -func (mock *MockMetricsServer) VerifyWasCalledInOrder(invocationCountMatcher pegomock.Matcher, inOrderContext *pegomock.InOrderContext) *VerifierMockMetricsServer { - return &VerifierMockMetricsServer{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - inOrderContext: inOrderContext, - } -} - -func (mock *MockMetricsServer) VerifyWasCalledEventually(invocationCountMatcher pegomock.Matcher, timeout time.Duration) *VerifierMockMetricsServer { - return &VerifierMockMetricsServer{ - mock: mock, - invocationCountMatcher: invocationCountMatcher, - timeout: timeout, - } -} - -type VerifierMockMetricsServer struct { - mock *MockMetricsServer - invocationCountMatcher pegomock.Matcher - inOrderContext *pegomock.InOrderContext - timeout time.Duration -} - -func (verifier *VerifierMockMetricsServer) ClusterLoad(_param0 *v1.NodeList, _param1 *v1beta1.NodeMetricsList, _param2 *client.ClusterMetrics) *MockMetricsServer_ClusterLoad_OngoingVerification { - params := []pegomock.Param{_param0, _param1, _param2} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "ClusterLoad", params, verifier.timeout) - return &MockMetricsServer_ClusterLoad_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockMetricsServer_ClusterLoad_OngoingVerification struct { - mock *MockMetricsServer - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockMetricsServer_ClusterLoad_OngoingVerification) GetCapturedArguments() (*v1.NodeList, *v1beta1.NodeMetricsList, *client.ClusterMetrics) { - _param0, _param1, _param2 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1], _param2[len(_param2)-1] -} - -func (c *MockMetricsServer_ClusterLoad_OngoingVerification) GetAllCapturedArguments() (_param0 []*v1.NodeList, _param1 []*v1beta1.NodeMetricsList, _param2 []*client.ClusterMetrics) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]*v1.NodeList, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(*v1.NodeList) - } - _param1 = make([]*v1beta1.NodeMetricsList, len(params[1])) - for u, param := range params[1] { - _param1[u] = param.(*v1beta1.NodeMetricsList) - } - _param2 = make([]*client.ClusterMetrics, len(params[2])) - for u, param := range params[2] { - _param2[u] = param.(*client.ClusterMetrics) - } - } - return -} - -func (verifier *VerifierMockMetricsServer) FetchNodesMetrics() *MockMetricsServer_FetchNodesMetrics_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "FetchNodesMetrics", params, verifier.timeout) - return &MockMetricsServer_FetchNodesMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockMetricsServer_FetchNodesMetrics_OngoingVerification struct { - mock *MockMetricsServer - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockMetricsServer_FetchNodesMetrics_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockMetricsServer_FetchNodesMetrics_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockMetricsServer) FetchPodsMetrics(_param0 string) *MockMetricsServer_FetchPodsMetrics_OngoingVerification { - params := []pegomock.Param{_param0} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "FetchPodsMetrics", params, verifier.timeout) - return &MockMetricsServer_FetchPodsMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockMetricsServer_FetchPodsMetrics_OngoingVerification struct { - mock *MockMetricsServer - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockMetricsServer_FetchPodsMetrics_OngoingVerification) GetCapturedArguments() string { - _param0 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1] -} - -func (c *MockMetricsServer_FetchPodsMetrics_OngoingVerification) GetAllCapturedArguments() (_param0 []string) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]string, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(string) - } - } - return -} - -func (verifier *VerifierMockMetricsServer) HasMetrics() *MockMetricsServer_HasMetrics_OngoingVerification { - params := []pegomock.Param{} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "HasMetrics", params, verifier.timeout) - return &MockMetricsServer_HasMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockMetricsServer_HasMetrics_OngoingVerification struct { - mock *MockMetricsServer - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockMetricsServer_HasMetrics_OngoingVerification) GetCapturedArguments() { -} - -func (c *MockMetricsServer_HasMetrics_OngoingVerification) GetAllCapturedArguments() { -} - -func (verifier *VerifierMockMetricsServer) NodesMetrics(_param0 *v1.NodeList, _param1 *v1beta1.NodeMetricsList, _param2 client.NodesMetrics) *MockMetricsServer_NodesMetrics_OngoingVerification { - params := []pegomock.Param{_param0, _param1, _param2} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "NodesMetrics", params, verifier.timeout) - return &MockMetricsServer_NodesMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockMetricsServer_NodesMetrics_OngoingVerification struct { - mock *MockMetricsServer - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockMetricsServer_NodesMetrics_OngoingVerification) GetCapturedArguments() (*v1.NodeList, *v1beta1.NodeMetricsList, client.NodesMetrics) { - _param0, _param1, _param2 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1], _param2[len(_param2)-1] -} - -func (c *MockMetricsServer_NodesMetrics_OngoingVerification) GetAllCapturedArguments() (_param0 []*v1.NodeList, _param1 []*v1beta1.NodeMetricsList, _param2 []client.NodesMetrics) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]*v1.NodeList, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(*v1.NodeList) - } - _param1 = make([]*v1beta1.NodeMetricsList, len(params[1])) - for u, param := range params[1] { - _param1[u] = param.(*v1beta1.NodeMetricsList) - } - _param2 = make([]client.NodesMetrics, len(params[2])) - for u, param := range params[2] { - _param2[u] = param.(client.NodesMetrics) - } - } - return -} - -func (verifier *VerifierMockMetricsServer) PodsMetrics(_param0 *v1beta1.PodMetricsList, _param1 client.PodsMetrics) *MockMetricsServer_PodsMetrics_OngoingVerification { - params := []pegomock.Param{_param0, _param1} - methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "PodsMetrics", params, verifier.timeout) - return &MockMetricsServer_PodsMetrics_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} -} - -type MockMetricsServer_PodsMetrics_OngoingVerification struct { - mock *MockMetricsServer - methodInvocations []pegomock.MethodInvocation -} - -func (c *MockMetricsServer_PodsMetrics_OngoingVerification) GetCapturedArguments() (*v1beta1.PodMetricsList, client.PodsMetrics) { - _param0, _param1 := c.GetAllCapturedArguments() - return _param0[len(_param0)-1], _param1[len(_param1)-1] -} - -func (c *MockMetricsServer_PodsMetrics_OngoingVerification) GetAllCapturedArguments() (_param0 []*v1beta1.PodMetricsList, _param1 []client.PodsMetrics) { - params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) - if len(params) > 0 { - _param0 = make([]*v1beta1.PodMetricsList, len(params[0])) - for u, param := range params[0] { - _param0[u] = param.(*v1beta1.PodMetricsList) - } - _param1 = make([]client.PodsMetrics, len(params[1])) - for u, param := range params[1] { - _param1[u] = param.(client.PodsMetrics) - } - } - return -} diff --git a/internal/model/pulse.go b/internal/model/pulse.go index d26ecc83f5..0b62a1a7c1 100644 --- a/internal/model/pulse.go +++ b/internal/model/pulse.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( diff --git a/internal/model/pulse_health.go b/internal/model/pulse_health.go index 8cd9b10e15..0f1891bb97 100644 --- a/internal/model/pulse_health.go +++ b/internal/model/pulse_health.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( @@ -7,9 +10,10 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/health" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/rs/zerolog/log" - metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -116,18 +120,18 @@ func (h *PulseHealth) check(ctx context.Context, ns, gvr string) (*health.Check, c := health.NewCheck(gvr) if meta.Renderer.IsGeneric() { - table, ok := oo[0].(*metav1beta1.Table) + table, ok := oo[0].(*metav1.Table) if !ok { return nil, fmt.Errorf("expecting a meta table but got %T", oo[0]) } - rows := make(render.Rows, len(table.Rows)) - re, _ := meta.Renderer.(Generic) + rows := make(model1.Rows, len(table.Rows)) + re, _ := meta.Renderer.(model1.Generic) re.SetTable(ns, table) for i, row := range table.Rows { if err := re.Render(row, ns, &rows[i]); err != nil { return nil, err } - if !render.Happy(ns, re.Header(ns), rows[i]) { + if !model1.IsValid(ns, re.Header(ns), rows[i]) { c.Inc(health.S2) continue } @@ -137,12 +141,12 @@ func (h *PulseHealth) check(ctx context.Context, ns, gvr string) (*health.Check, return c, nil } c.Total(int64(len(oo))) - rr, re := make(render.Rows, len(oo)), meta.Renderer + rr, re := make(model1.Rows, len(oo)), meta.Renderer for i, o := range oo { if err := re.Render(o, ns, &rr[i]); err != nil { return nil, err } - if !render.Happy(ns, re.Header(ns), rr[i]) { + if !model1.IsValid(ns, re.Header(ns), rr[i]) { c.Inc(health.S2) continue } diff --git a/internal/model/registry.go b/internal/model/registry.go index bce63731f6..97c2539455 100644 --- a/internal/model/registry.go +++ b/internal/model/registry.go @@ -1,14 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/render/helm" "github.com/derailed/k9s/internal/xray" ) // Registry tracks resources metadata. // BOZO!! Break up deps and merge into single registrar. var Registry = map[string]ResourceMeta{ + "workloads": { + DAO: &dao.Workload{}, + Renderer: &render.Workload{}, + }, // Custom... "references": { DAO: &dao.Reference{}, @@ -22,19 +30,22 @@ var Registry = map[string]ResourceMeta{ DAO: &dao.Pulse{}, }, "helm": { - DAO: &dao.Helm{}, - Renderer: &render.Helm{}, - }, - // BOZO!! revamp with latest... - // "openfaas": { - // DAO: &dao.OpenFaas{}, - // Renderer: &render.OpenFaas{}, - // }, + DAO: &dao.HelmChart{}, + Renderer: &helm.Chart{}, + }, + "helm-history": { + DAO: &dao.HelmHistory{}, + Renderer: &helm.History{}, + }, "containers": { DAO: &dao.Container{}, Renderer: &render.Container{}, TreeRenderer: &xray.Container{}, }, + "scans": { + DAO: &dao.ImageScan{}, + Renderer: &render.ImageScan{}, + }, "contexts": { DAO: &dao.Context{}, Renderer: &render.Context{}, @@ -71,14 +82,15 @@ var Registry = map[string]ResourceMeta{ DAO: &dao.Alias{}, Renderer: &render.Alias{}, }, - "popeye": { - DAO: &dao.Popeye{}, - Renderer: &render.Popeye{}, - }, - "sanitizer": { - DAO: &dao.Popeye{}, - TreeRenderer: &xray.Section{}, - }, + // !!BOZO!! Popeye + //"popeye": { + // DAO: &dao.Popeye{}, + // Renderer: &render.Popeye{}, + //}, + //"sanitizer": { + // DAO: &dao.Popeye{}, + // TreeRenderer: &xray.Section{}, + //}, // Core... "v1/endpoints": { @@ -90,8 +102,17 @@ var Registry = map[string]ResourceMeta{ TreeRenderer: &xray.Pod{}, }, "v1/namespaces": { + DAO: &dao.Namespace{}, Renderer: &render.Namespace{}, }, + "v1/secrets": { + DAO: &dao.Secret{}, + Renderer: &render.Secret{}, + }, + "v1/configmaps": { + DAO: &dao.ConfigMap{}, + Renderer: &render.ConfigMap{}, + }, "v1/nodes": { DAO: &dao.Node{}, Renderer: &render.Node{}, @@ -101,6 +122,10 @@ var Registry = map[string]ResourceMeta{ Renderer: &render.Service{}, TreeRenderer: &xray.Service{}, }, + "v1/events": { + DAO: &dao.Table{}, + Renderer: &render.Event{}, + }, "v1/serviceaccounts": { Renderer: &render.ServiceAccount{}, }, @@ -110,10 +135,6 @@ var Registry = map[string]ResourceMeta{ "v1/persistentvolumeclaims": { Renderer: &render.PersistentVolumeClaim{}, }, - "v1/events": { - DAO: &dao.Table{}, - Renderer: &render.Event{}, - }, // Apps... "apps/v1/deployments": { @@ -153,6 +174,7 @@ var Registry = map[string]ResourceMeta{ // CRDs... "apiextensions.k8s.io/v1/customresourcedefinitions": { + DAO: &dao.CustomResourceDefinition{}, Renderer: &render.CustomResourceDefinition{}, }, @@ -162,7 +184,7 @@ var Registry = map[string]ResourceMeta{ }, // Policy... - "policy/v1beta1/poddisruptionbudgets": { + "policy/v1/poddisruptionbudgets": { Renderer: &render.PodDisruptionBudget{}, }, diff --git a/internal/model/rev_values.go b/internal/model/rev_values.go new file mode 100644 index 0000000000..a0b8cb98c5 --- /dev/null +++ b/internal/model/rev_values.go @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model + +import ( + "context" + "strings" + "sync/atomic" + "time" + + backoff "github.com/cenkalti/backoff/v4" + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/rs/zerolog/log" + "github.com/sahilm/fuzzy" +) + +// RevValues tracks Helm values representations. +type RevValues struct { + gvr client.GVR + inUpdate int32 + path string + rev string + query string + lines []string + allValues bool + listeners []ResourceViewerListener + options ViewerToggleOpts +} + +// NewRevValues return a new Helm values resource model. +func NewRevValues(gvr client.GVR, path, rev string) *RevValues { + return &RevValues{ + gvr: gvr, + path: path, + rev: rev, + allValues: false, + lines: getRevValues(path, rev), + } +} + +func getHelmHistDao() *dao.HelmHistory { + return Registry["helm-history"].DAO.(*dao.HelmHistory) +} + +func getRevValues(path, rev string) []string { + vals, err := getHelmHistDao().GetValues(path, true) + if err != nil { + log.Error().Err(err).Msgf("Failed to get Helm values") + } + return strings.Split(string(vals), "\n") +} + +// GVR returns the resource gvr. +func (v *RevValues) GVR() client.GVR { + return v.gvr +} + +// GetPath returns the active resource path. +func (v *RevValues) GetPath() string { + return v.path +} + +// SetOptions toggle model options. +func (v *RevValues) SetOptions(ctx context.Context, opts ViewerToggleOpts) { + v.options = opts + if err := v.refresh(ctx); err != nil { + v.fireResourceFailed(err) + } +} + +// Filter filters the model. +func (v *RevValues) Filter(q string) { + v.query = q + v.filterChanged(v.lines) +} + +func (v *RevValues) filterChanged(lines []string) { + v.fireResourceChanged(lines, v.filter(v.query, lines)) +} + +func (v *RevValues) filter(q string, lines []string) fuzzy.Matches { + if q == "" { + return nil + } + if f, ok := internal.IsFuzzySelector(q); ok { + return v.fuzzyFilter(strings.TrimSpace(f), lines) + } + return rxFilter(q, lines) +} + +func (*RevValues) fuzzyFilter(q string, lines []string) fuzzy.Matches { + return fuzzy.Find(q, lines) +} + +func (v *RevValues) fireResourceChanged(lines []string, matches fuzzy.Matches) { + for _, l := range v.listeners { + l.ResourceChanged(lines, matches) + } +} + +func (v *RevValues) fireResourceFailed(err error) { + for _, l := range v.listeners { + l.ResourceFailed(err) + } +} + +// ClearFilter clear out the filter. +func (v *RevValues) ClearFilter() { + v.query = "" +} + +// Peek returns the current model data. +func (v *RevValues) Peek() []string { + return v.lines +} + +// Refresh updates model data. +func (v *RevValues) Refresh(ctx context.Context) error { + return v.refresh(ctx) +} + +// Watch watches for Values changes. +func (v *RevValues) Watch(ctx context.Context) error { + if err := v.refresh(ctx); err != nil { + return err + } + go v.updater(ctx) + + return nil +} + +func (v *RevValues) updater(ctx context.Context) { + defer log.Debug().Msgf("YAML canceled -- %q", v.gvr) + + backOff := NewExpBackOff(ctx, defaultReaderRefreshRate, maxReaderRetryInterval) + delay := defaultReaderRefreshRate + for { + select { + case <-ctx.Done(): + return + case <-time.After(delay): + if err := v.refresh(ctx); err != nil { + v.fireResourceFailed(err) + if delay = backOff.NextBackOff(); delay == backoff.Stop { + log.Error().Err(err).Msgf("giving up retrieving chart values") + return + } + } else { + backOff.Reset() + delay = defaultReaderRefreshRate + } + } + } +} + +func (v *RevValues) refresh(ctx context.Context) error { + if !atomic.CompareAndSwapInt32(&v.inUpdate, 0, 1) { + log.Debug().Msgf("Dropping update...") + return nil + } + defer atomic.StoreInt32(&v.inUpdate, 0) + + if err := v.reconcile(ctx); err != nil { + return err + } + + return nil +} + +func (v *RevValues) reconcile(_ context.Context) error { + v.fireResourceChanged(v.lines, v.filter(v.query, v.lines)) + + return nil +} + +// AddListener adds a new model listener. +func (v *RevValues) AddListener(l ResourceViewerListener) { + v.listeners = append(v.listeners, l) +} + +// RemoveListener delete a listener from the list. +func (v *RevValues) RemoveListener(l ResourceViewerListener) { + victim := -1 + for i, lis := range v.listeners { + if lis == l { + victim = i + break + } + } + + if victim >= 0 { + v.listeners = append(v.listeners[:victim], v.listeners[victim+1:]...) + } +} diff --git a/internal/model/semver.go b/internal/model/semver.go index d2fea1e9d6..4b88794edf 100644 --- a/internal/model/semver.go +++ b/internal/model/semver.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( diff --git a/internal/model/semver_test.go b/internal/model/semver_test.go index 6ab3fd5bd0..45876f1e1b 100644 --- a/internal/model/semver_test.go +++ b/internal/model/semver_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model_test import ( diff --git a/internal/model/stack.go b/internal/model/stack.go index 53cd0feaca..55221b8560 100644 --- a/internal/model/stack.go +++ b/internal/model/stack.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( @@ -109,6 +112,7 @@ func (s *Stack) Pop() (Component, bool) { s.mx.Lock() { c = s.components[len(s.components)-1] + c.Stop() s.components = s.components[:len(s.components)-1] } s.mx.Unlock() @@ -163,6 +167,8 @@ func (s *Stack) Top() Component { return nil } + s.mx.RLock() + defer s.mx.RUnlock() return s.components[len(s.components)-1] } diff --git a/internal/model/stack_test.go b/internal/model/stack_test.go index 9ccc5ed6cb..5a3eca62f4 100644 --- a/internal/model/stack_test.go +++ b/internal/model/stack_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model_test import ( @@ -298,11 +301,13 @@ func (c c) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { return func (c c) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { return nil } -func (c c) SetRect(int, int, int, int) {} -func (c c) GetRect() (int, int, int, int) { return 0, 0, 0, 0 } -func (c c) GetFocusable() tview.Focusable { return nil } -func (c c) Focus(func(tview.Primitive)) {} -func (c c) Blur() {} -func (c c) Start() {} -func (c c) Stop() {} -func (c c) Init(context.Context) error { return nil } +func (c c) SetRect(int, int, int, int) {} +func (c c) GetRect() (int, int, int, int) { return 0, 0, 0, 0 } +func (c c) GetFocusable() tview.Focusable { return nil } +func (c c) Focus(func(tview.Primitive)) {} +func (c c) Blur() {} +func (c c) Start() {} +func (c c) Stop() {} +func (c c) Init(context.Context) error { return nil } +func (c c) SetFilter(string) {} +func (c c) SetLabelFilter(map[string]string) {} diff --git a/internal/model/table.go b/internal/model/table.go index 861fd1eb5b..be1157b587 100644 --- a/internal/model/table.go +++ b/internal/model/table.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( @@ -11,10 +14,9 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" "github.com/rs/zerolog/log" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" "k8s.io/apimachinery/pkg/runtime" ) @@ -23,7 +25,7 @@ const initRefreshRate = 300 * time.Millisecond // TableListener represents a table model listener. type TableListener interface { // TableDataChanged notifies the model data changed. - TableDataChanged(*render.TableData) + TableDataChanged(*model1.TableData) // TableLoadFailed notifies the load failed. TableLoadFailed(error) @@ -32,21 +34,20 @@ type TableListener interface { // Table represents a table model. type Table struct { gvr client.GVR - namespace string - data *render.TableData + data *model1.TableData listeners []TableListener inUpdate int32 refreshRate time.Duration instance string - mx sync.RWMutex labelFilter string + mx sync.RWMutex } // NewTable returns a new table model. func NewTable(gvr client.GVR) *Table { return &Table{ gvr: gvr, - data: render.NewTableData(), + data: model1.NewTableData(gvr), refreshRate: 2 * time.Second, } } @@ -54,8 +55,17 @@ func NewTable(gvr client.GVR) *Table { // SetLabelFilter sets the labels filter. func (t *Table) SetLabelFilter(f string) { t.mx.Lock() + defer t.mx.Unlock() + t.labelFilter = f - t.mx.Unlock() +} + +// GetLabelFilter sets the labels filter. +func (t *Table) GetLabelFilter() string { + t.mx.Lock() + defer t.mx.Unlock() + + return t.labelFilter } // SetInstance sets a single entry table. @@ -65,6 +75,9 @@ func (t *Table) SetInstance(path string) { // AddListener adds a new model listener. func (t *Table) AddListener(l TableListener) { + t.mx.Lock() + defer t.mx.Unlock() + t.listeners = append(t.listeners, l) } @@ -80,8 +93,8 @@ func (t *Table) RemoveListener(l TableListener) { if victim >= 0 { t.mx.Lock() - defer t.mx.Unlock() t.listeners = append(t.listeners[:victim], t.listeners[victim+1:]...) + t.mx.Unlock() } } @@ -127,18 +140,17 @@ func (t *Table) Delete(ctx context.Context, path string, propagation *metav1.Del // GetNamespace returns the model namespace. func (t *Table) GetNamespace() string { - return t.namespace + return t.data.GetNamespace() } // SetNamespace sets up model namespace. func (t *Table) SetNamespace(ns string) { - t.namespace = ns - t.data.Clear() + t.data.Reset(ns) } // InNamespace checks if current namespace matches desired namespace. func (t *Table) InNamespace(ns string) bool { - return len(t.data.RowEvents) > 0 && t.namespace == ns + return t.data.GetNamespace() == ns && !t.data.Empty() } // SetRefreshRate sets model refresh duration. @@ -148,7 +160,7 @@ func (t *Table) SetRefreshRate(d time.Duration) { // ClusterWide checks if resource is scope for all namespaces. func (t *Table) ClusterWide() bool { - return client.IsClusterWide(t.namespace) + return client.IsClusterWide(t.data.GetNamespace()) } // Empty returns true if no model data. @@ -156,13 +168,13 @@ func (t *Table) Empty() bool { return t.data.Empty() } -// Count returns the row count. -func (t *Table) Count() int { - return t.data.Count() +// RowCount returns the row count. +func (t *Table) RowCount() int { + return t.data.RowCount() } // Peek returns model data. -func (t *Table) Peek() *render.TableData { +func (t *Table) Peek() *model1.TableData { t.mx.RLock() defer t.mx.RUnlock() @@ -170,8 +182,6 @@ func (t *Table) Peek() *render.TableData { } func (t *Table) updater(ctx context.Context) { - defer log.Debug().Msgf("TABLE-UPDATER canceled -- %q", t.gvr) - bf := backoff.NewExponentialBackOff() bf.InitialInterval, bf.MaxElapsedTime = initRefreshRate, maxReaderRetryInterval rate := initRefreshRate @@ -185,7 +195,7 @@ func (t *Table) updater(ctx context.Context) { return t.refresh(ctx) }, backoff.WithContext(bf, ctx)) if err != nil { - log.Error().Err(err).Msgf("Retry failed") + log.Warn().Err(err).Msgf("reconciler exited") t.fireTableLoadFailed(err) return } @@ -194,6 +204,10 @@ func (t *Table) updater(ctx context.Context) { } func (t *Table) refresh(ctx context.Context) error { + defer func(ti time.Time) { + log.Trace().Msgf("Refresh [%s](%d) %s ", t.gvr, t.data.RowCount(), time.Since(ti)) + }(time.Now()) + if !atomic.CompareAndSwapInt32(&t.inUpdate, 0, 1) { log.Debug().Msgf("Dropping update...") return nil @@ -215,25 +229,25 @@ func (t *Table) list(ctx context.Context, a dao.Accessor) ([]runtime.Object, err } a.Init(factory, t.gvr) - ns := client.CleanseNamespace(t.namespace) - if client.IsClusterScoped(t.namespace) { - ns = client.AllNamespaces + t.mx.RLock() + ctx = context.WithValue(ctx, internal.KeyLabels, t.labelFilter) + t.mx.RUnlock() + + ns := client.CleanseNamespace(t.data.GetNamespace()) + if client.IsClusterScoped(ns) { + ns = client.BlankNamespace } return a.List(ctx, ns) } func (t *Table) reconcile(ctx context.Context) error { - t.mx.Lock() - defer t.mx.Unlock() - meta := resourceMeta(t.gvr) - if t.labelFilter != "" { - ctx = context.WithValue(ctx, internal.KeyLabels, t.labelFilter) - } var ( oo []runtime.Object err error ) + meta := resourceMeta(t.gvr) + ctx = context.WithValue(ctx, internal.KeyLabels, t.labelFilter) if t.instance == "" { oo, err = t.list(ctx, meta.DAO) } else { @@ -244,91 +258,27 @@ func (t *Table) reconcile(ctx context.Context) error { return err } - var rows render.Rows - if len(oo) > 0 { - if meta.Renderer.IsGeneric() { - table, ok := oo[0].(*metav1beta1.Table) - if !ok { - return fmt.Errorf("expecting a meta table but got %T", oo[0]) - } - rows = make(render.Rows, len(table.Rows)) - if err := genericHydrate(t.namespace, table, rows, meta.Renderer); err != nil { - return err - } - } else { - rows = make(render.Rows, len(oo)) - if err := hydrate(t.namespace, oo, rows, meta.Renderer); err != nil { - return err - } - } - } - - // if labelSelector in place might as well clear the model data. - sel, ok := ctx.Value(internal.KeyLabels).(string) - if ok && sel != "" { - t.data.Clear() - } - t.data.Update(rows) - t.data.SetHeader(t.namespace, meta.Renderer.Header(t.namespace)) - - if len(t.data.Header) == 0 { - return fmt.Errorf("fail to list resource %s", t.gvr) - } - - return nil + return t.data.Reconcile(ctx, meta.Renderer, oo) } -func (t *Table) fireTableChanged(data *render.TableData) { +func (t *Table) fireTableChanged(data *model1.TableData) { + var ll []TableListener t.mx.RLock() - defer t.mx.RUnlock() + ll = t.listeners + t.mx.RUnlock() - for _, l := range t.listeners { + for _, l := range ll { l.TableDataChanged(data) } } func (t *Table) fireTableLoadFailed(err error) { - for _, l := range t.listeners { - l.TableLoadFailed(err) - } -} - -// ---------------------------------------------------------------------------- -// Helpers... - -func hydrate(ns string, oo []runtime.Object, rr render.Rows, re Renderer) error { - for i, o := range oo { - if err := re.Render(o, ns, &rr[i]); err != nil { - return err - } - } - - return nil -} - -// Generic represents a generic resource. -type Generic interface { - // SetTable sets up the resource tabular definition. - SetTable(ns string, table *metav1beta1.Table) - - // Header returns a resource header. - Header(ns string) render.Header - - // Render renders the resource. - Render(o interface{}, ns string, row *render.Row) error -} + var ll []TableListener + t.mx.RLock() + ll = t.listeners + t.mx.RUnlock() -func genericHydrate(ns string, table *metav1beta1.Table, rr render.Rows, re Renderer) error { - gr, ok := re.(Generic) - if !ok { - return fmt.Errorf("expecting generic renderer but got %T", re) - } - gr.SetTable(ns, table) - for i, row := range table.Rows { - if err := gr.Render(row, ns, &rr[i]); err != nil { - return err - } + for _, l := range ll { + l.TableLoadFailed(err) } - - return nil } diff --git a/internal/model/table_int_test.go b/internal/model/table_int_test.go index 7667efb8b6..ea7c03904e 100644 --- a/internal/model/table_int_test.go +++ b/internal/model/table_int_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( @@ -10,11 +13,11 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/watch" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/informers" @@ -32,9 +35,9 @@ func TestTableReconcile(t *testing.T) { err := ta.reconcile(ctx) assert.Nil(t, err) data := ta.Peek() - assert.Equal(t, 22, len(data.Header)) - assert.Equal(t, 1, len(data.RowEvents)) - assert.Equal(t, client.NamespaceAll, data.Namespace) + assert.Equal(t, 23, data.HeaderCount()) + assert.Equal(t, 1, data.RowCount()) + assert.Equal(t, client.NamespaceAll, data.GetNamespace()) } func TestTableList(t *testing.T) { @@ -63,12 +66,10 @@ func TestTableGet(t *testing.T) { } func TestTableMeta(t *testing.T) { - pd := dao.Pod{} - pd.Init(makeFactory(), client.NewGVR("v1/pods")) uu := map[string]struct { gvr string accessor dao.Accessor - renderer Renderer + renderer model1.Renderer }{ "generic": { gvr: "containers", @@ -81,9 +82,9 @@ func TestTableMeta(t *testing.T) { renderer: &render.Node{}, }, "table": { - gvr: "v1/configmaps", + gvr: "v1/events", accessor: &dao.Table{}, - renderer: &render.Generic{}, + renderer: &render.Event{}, }, } @@ -97,44 +98,6 @@ func TestTableMeta(t *testing.T) { } } -func TestTableHydrate(t *testing.T) { - oo := []runtime.Object{ - &render.PodWithMetrics{Raw: load(t, "p1")}, - } - rr := make([]render.Row, 1) - - assert.Nil(t, hydrate("blee", oo, rr, render.Pod{})) - assert.Equal(t, 1, len(rr)) - assert.Equal(t, 22, len(rr[0].Fields)) -} - -func TestTableGenericHydrate(t *testing.T) { - raw := raw(t, "p1") - tt := metav1beta1.Table{ - ColumnDefinitions: []metav1beta1.TableColumnDefinition{ - {Name: "c1"}, - {Name: "c2"}, - }, - Rows: []metav1beta1.TableRow{ - { - Cells: []interface{}{"fred", 10}, - Object: runtime.RawExtension{Raw: raw}, - }, - { - Cells: []interface{}{"blee", 20}, - Object: runtime.RawExtension{Raw: raw}, - }, - }, - } - rr := make([]render.Row, 2) - re := render.Generic{} - re.SetTable("blee", &tt) - - assert.Nil(t, genericHydrate("blee", &tt, rr, &re)) - assert.Equal(t, 2, len(rr)) - assert.Equal(t, 3, len(rr[0].Fields)) -} - // ---------------------------------------------------------------------------- // Helpers... @@ -159,12 +122,6 @@ func load(t *testing.T, n string) *unstructured.Unstructured { return &o } -func raw(t *testing.T, n string) []byte { - raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n)) - assert.Nil(t, err) - return raw -} - // ---------------------------------------------------------------------------- func makeFactory() testFactory { diff --git a/internal/model/table_test.go b/internal/model/table_test.go index 55d7261a87..5417101778 100644 --- a/internal/model/table_test.go +++ b/internal/model/table_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model_test import ( @@ -11,7 +14,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/watch" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -33,9 +36,9 @@ func TestTableRefresh(t *testing.T) { ctx = context.WithValue(ctx, internal.KeyWithMetrics, false) assert.NoError(t, ta.Refresh(ctx)) data := ta.Peek() - assert.Equal(t, 22, len(data.Header)) - assert.Equal(t, 1, len(data.RowEvents)) - assert.Equal(t, client.NamespaceAll, data.Namespace) + assert.Equal(t, 23, data.HeaderCount()) + assert.Equal(t, 1, data.RowCount()) + assert.Equal(t, client.NamespaceAll, data.GetNamespace()) assert.Equal(t, 1, l.count) assert.Equal(t, 0, l.errs) } @@ -72,7 +75,7 @@ type tableListener struct { count, errs int } -func (l *tableListener) TableDataChanged(*render.TableData) { +func (l *tableListener) TableDataChanged(*model1.TableData) { l.count++ } diff --git a/internal/model/text.go b/internal/model/text.go index 7b1f5e2b82..b50c8680af 100644 --- a/internal/model/text.go +++ b/internal/model/text.go @@ -1,10 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( - "regexp" "strings" - "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal" "github.com/sahilm/fuzzy" ) @@ -109,27 +111,12 @@ func (t *Text) filter(q string, lines []string) fuzzy.Matches { if q == "" { return nil } - if dao.IsFuzzySelector(q) { - return t.fuzzyFilter(strings.TrimSpace(q[2:]), lines) + if f, ok := internal.IsFuzzySelector(q); ok { + return t.fuzzyFilter(strings.TrimSpace(f), lines) } - return t.rxFilter(q, lines) + return rxFilter(q, lines) } func (*Text) fuzzyFilter(q string, lines []string) fuzzy.Matches { return fuzzy.Find(q, lines) } - -func (*Text) rxFilter(q string, lines []string) fuzzy.Matches { - rx, err := regexp.Compile(`(?i)` + q) - if err != nil { - return nil - } - matches := make(fuzzy.Matches, 0, len(lines)) - for i, l := range lines { - if loc := rx.FindStringIndex(l); len(loc) == 2 { - matches = append(matches, fuzzy.Match{Str: q, Index: i, MatchedIndexes: loc}) - } - } - - return matches -} diff --git a/internal/model/text_test.go b/internal/model/text_test.go index d8c2f94923..ffa390cd41 100644 --- a/internal/model/text_test.go +++ b/internal/model/text_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model_test import ( diff --git a/internal/model/tree.go b/internal/model/tree.go index 3287d40616..c013238e85 100644 --- a/internal/model/tree.go +++ b/internal/model/tree.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( @@ -14,7 +17,7 @@ import ( "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/xray" "github.com/rs/zerolog/log" - metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -210,7 +213,7 @@ func (t *Tree) reconcile(ctx context.Context) error { root := xray.NewTreeNode(res, res) ctx = context.WithValue(ctx, xray.KeyParent, root) if _, ok := meta.TreeRenderer.(*xray.Generic); ok { - table, ok := oo[0].(*metav1beta1.Table) + table, ok := oo[0].(*metav1.Table) if !ok { return fmt.Errorf("expecting a Table but got %T", oo[0]) } @@ -223,7 +226,7 @@ func (t *Tree) reconcile(ctx context.Context) error { root.Sort() if t.query != "" { - t.root = root.Filter(t.query, rxFilter) + t.root = root.Filter(t.query, rxMatch) } if t.root == nil || t.root.Diff(root) { t.root = root @@ -274,7 +277,7 @@ func (t *Tree) getMeta(ctx context.Context, gvr string) (ResourceMeta, error) { // ---------------------------------------------------------------------------- // Helpers... -func rxFilter(q, path string) bool { +func rxMatch(q, path string) bool { rx := regexp.MustCompile(`(?i)` + q) tokens := strings.Split(path, "::") @@ -299,7 +302,7 @@ func treeHydrate(ctx context.Context, ns string, oo []runtime.Object, re TreeRen return nil } -func genericTreeHydrate(ctx context.Context, ns string, table *metav1beta1.Table, re TreeRenderer) error { +func genericTreeHydrate(ctx context.Context, ns string, table *metav1.Table, re TreeRenderer) error { tre, ok := re.(*xray.Generic) if !ok { return fmt.Errorf("expecting xray.Generic renderer but got %T", re) diff --git a/internal/model/types.go b/internal/model/types.go index 489fedd6ea..e729b32204 100644 --- a/internal/model/types.go +++ b/internal/model/types.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( @@ -6,7 +9,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tview" "github.com/sahilm/fuzzy" "k8s.io/apimachinery/pkg/runtime" @@ -30,6 +33,7 @@ type ViewerToggleOpts map[string]bool type ResourceViewer interface { GetPath() string Filter(string) + GVR() client.GVR ClearFilter() Peek() []string SetOptions(context.Context, ViewerToggleOpts) @@ -39,6 +43,14 @@ type ResourceViewer interface { RemoveListener(ResourceViewerListener) } +// EncDecResourceViewer interface extends the ResourceViewer interface and +// adds a `Toggle` that allows the user to switch between encoded or decoded +// state of the view. +type EncDecResourceViewer interface { + ResourceViewer + Toggle() +} + // Igniter represents a runnable view. type Igniter interface { // Start starts a component. @@ -80,21 +92,12 @@ type Component interface { Igniter Hinter Commander + Filterer } -// Renderer represents a resource renderer. -type Renderer interface { - // IsGeneric identifies a generic handler. - IsGeneric() bool - - // Render converts raw resources to tabular data. - Render(o interface{}, ns string, row *render.Row) error - - // Header returns the resource header. - Header(ns string) render.Header - - // ColorerFunc returns a row colorer function. - ColorerFunc() render.ColorerFunc +type Filterer interface { + SetFilter(string) + SetLabelFilter(map[string]string) } // Cruder performs crud operations. @@ -131,6 +134,6 @@ type TreeRenderer interface { // ResourceMeta represents model info about a resource. type ResourceMeta struct { DAO dao.Accessor - Renderer Renderer + Renderer model1.Renderer TreeRenderer TreeRenderer } diff --git a/internal/model/values.go b/internal/model/values.go index 46022bc287..eafe098d30 100644 --- a/internal/model/values.go +++ b/internal/model/values.go @@ -1,13 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( "context" - "regexp" + "fmt" "strings" "sync/atomic" "time" backoff "github.com/cenkalti/backoff/v4" + "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/rs/zerolog/log" @@ -16,6 +20,7 @@ import ( // Values tracks Helm values representations. type Values struct { + factory dao.Factory gvr client.GVR inUpdate int32 path string @@ -32,27 +37,54 @@ func NewValues(gvr client.GVR, path string) *Values { gvr: gvr, path: path, allValues: false, - lines: getValues(path, false), } } -func getHelmDao() *dao.Helm { - return Registry["helm"].DAO.(*dao.Helm) +// Init initializes the model. +func (v *Values) Init(f dao.Factory) error { + v.factory = f + + var err error + v.lines, err = v.getValues() + + return err } -func getValues(path string, allValues bool) []string { - vals, err := getHelmDao().GetValues(path, allValues) +func (v *Values) getValues() ([]string, error) { + accessor, err := dao.AccessorFor(v.factory, v.gvr) if err != nil { - log.Error().Err(err).Msgf("Failed to get Helm values") + return nil, err } - return strings.Split(string(vals), "\n") + + valuer, ok := accessor.(dao.Valuer) + if !ok { + return nil, fmt.Errorf("Resource %s is not Valuer", v.gvr) + } + + values, err := valuer.GetValues(v.path, v.allValues) + if err != nil { + return nil, err + } + + return strings.Split(string(values), "\n"), nil +} + +// GVR returns the resource gvr. +func (v *Values) GVR() client.GVR { + return v.gvr } // ToggleValues toggles between user supplied values and computed values. -func (v *Values) ToggleValues() { +func (v *Values) ToggleValues() error { v.allValues = !v.allValues - lines := getValues(v.path, v.allValues) + + lines, err := v.getValues() + if err != nil { + return err + } + v.lines = lines + return nil } // GetPath returns the active resource path. @@ -82,31 +114,16 @@ func (v *Values) filter(q string, lines []string) fuzzy.Matches { if q == "" { return nil } - if dao.IsFuzzySelector(q) { - return v.fuzzyFilter(strings.TrimSpace(q[2:]), lines) + if f, ok := internal.IsFuzzySelector(q); ok { + return v.fuzzyFilter(strings.TrimSpace(f), lines) } - return v.rxFilter(q, lines) + return rxFilter(q, lines) } func (*Values) fuzzyFilter(q string, lines []string) fuzzy.Matches { return fuzzy.Find(q, lines) } -func (*Values) rxFilter(q string, lines []string) fuzzy.Matches { - rx, err := regexp.Compile(`(?i)` + q) - if err != nil { - return nil - } - matches := make(fuzzy.Matches, 0, len(lines)) - for i, l := range lines { - if loc := rx.FindStringIndex(l); len(loc) == 2 { - matches = append(matches, fuzzy.Match{Str: q, Index: i, MatchedIndexes: loc}) - } - } - - return matches -} - func (v *Values) fireResourceChanged(lines []string, matches fuzzy.Matches) { for _, l := range v.listeners { l.ResourceChanged(lines, matches) diff --git a/internal/model/yaml.go b/internal/model/yaml.go index e919b26fce..e476e27288 100644 --- a/internal/model/yaml.go +++ b/internal/model/yaml.go @@ -1,10 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package model import ( "context" "fmt" "reflect" - "regexp" "strings" "sync/atomic" "time" @@ -40,6 +42,11 @@ func NewYAML(gvr client.GVR, path string) *YAML { } } +// GVR returns the resource gvr. +func (y *YAML) GVR() client.GVR { + return y.gvr +} + // GetPath returns the active resource path. func (y *YAML) GetPath() string { return y.path @@ -67,31 +74,16 @@ func (y *YAML) filter(q string, lines []string) fuzzy.Matches { if q == "" { return nil } - if dao.IsFuzzySelector(q) { - return y.fuzzyFilter(strings.TrimSpace(q[2:]), lines) + if f, ok := internal.IsFuzzySelector(q); ok { + return y.fuzzyFilter(strings.TrimSpace(f), lines) } - return y.rxFilter(q, lines) + return rxFilter(q, lines) } func (*YAML) fuzzyFilter(q string, lines []string) fuzzy.Matches { return fuzzy.Find(q, lines) } -func (*YAML) rxFilter(q string, lines []string) fuzzy.Matches { - rx, err := regexp.Compile(`(?i)` + q) - if err != nil { - return nil - } - matches := make(fuzzy.Matches, 0, len(lines)) - for i, l := range lines { - locs := rx.FindAllStringIndex(l, -1) - for _, loc := range locs { - matches = append(matches, fuzzy.Match{Str: q, Index: i, MatchedIndexes: loc}) - } - } - return matches -} - func (y *YAML) fireResourceChanged(lines []string, matches fuzzy.Matches) { for _, l := range y.listeners { l.ResourceChanged(lines, matches) diff --git a/internal/render/color.go b/internal/model1/color.go similarity index 73% rename from internal/render/color.go rename to internal/model1/color.go index cc1339b210..f570a0409e 100644 --- a/internal/render/color.go +++ b/internal/model1/color.go @@ -1,8 +1,9 @@ -package render +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s -import ( - "github.com/derailed/tcell/v2" -) +package model1 + +import "github.com/derailed/tcell/v2" var ( // ModColor row modified color. @@ -30,12 +31,9 @@ var ( CompletedColor tcell.Color ) -// ColorerFunc represents a resource row colorer. -type ColorerFunc func(ns string, h Header, re RowEvent) tcell.Color - // DefaultColorer set the default table row colors. -func DefaultColorer(ns string, h Header, re RowEvent) tcell.Color { - if !Happy(ns, h, re.Row) { +func DefaultColorer(ns string, h Header, re *RowEvent) tcell.Color { + if !IsValid(ns, h, re.Row) { return ErrColor } diff --git a/internal/model1/color_test.go b/internal/model1/color_test.go new file mode 100644 index 0000000000..985bab95a9 --- /dev/null +++ b/internal/model1/color_test.go @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/model1" + "github.com/derailed/tcell/v2" + "github.com/stretchr/testify/assert" +) + +func TestDefaultColorer(t *testing.T) { + uu := map[string]struct { + re model1.RowEvent + e tcell.Color + }{ + "add": { + model1.RowEvent{ + Kind: model1.EventAdd, + }, + model1.AddColor, + }, + "update": { + model1.RowEvent{ + Kind: model1.EventUpdate, + }, + model1.ModColor, + }, + "delete": { + model1.RowEvent{ + Kind: model1.EventDelete, + }, + model1.KillColor, + }, + "no-change": { + model1.RowEvent{ + Kind: model1.EventUnchanged, + }, + model1.StdColor, + }, + "invalid": { + model1.RowEvent{ + Kind: model1.EventUnchanged, + Row: model1.Row{ + Fields: model1.Fields{"", "", "blah"}, + }, + }, + model1.ErrColor, + }, + } + + h := model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "VALID"}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, model1.DefaultColorer("", h, &u.re)) + }) + } +} diff --git a/internal/render/delta.go b/internal/model1/delta.go similarity index 94% rename from internal/render/delta.go rename to internal/model1/delta.go index 557dd25fdd..3d3e32dc84 100644 --- a/internal/render/delta.go +++ b/internal/model1/delta.go @@ -1,8 +1,9 @@ -package render +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s -import ( - "reflect" -) +package model1 + +import "reflect" // DeltaRow represents a collection of row deltas between old and new row. type DeltaRow []string diff --git a/internal/model1/delta_test.go b/internal/model1/delta_test.go new file mode 100644 index 0000000000..a809660c4c --- /dev/null +++ b/internal/model1/delta_test.go @@ -0,0 +1,266 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/model1" + "github.com/stretchr/testify/assert" +) + +func TestDeltaLabelize(t *testing.T) { + uu := map[string]struct { + o model1.Row + n model1.Row + e model1.DeltaRow + }{ + "same": { + o: model1.Row{ + Fields: model1.Fields{"a", "b", "blee=fred,doh=zorg"}, + }, + n: model1.Row{ + Fields: model1.Fields{"a", "b", "blee=fred1,doh=zorg"}, + }, + e: model1.DeltaRow{"", "", "fred", "zorg"}, + }, + } + + hh := model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "C"}, + } + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + d := model1.NewDeltaRow(u.o, u.n, hh) + d = d.Labelize([]int{0, 1}, 2) + assert.Equal(t, u.e, d) + }) + } +} + +func TestDeltaCustomize(t *testing.T) { + uu := map[string]struct { + r1, r2 model1.Row + cols []int + e model1.DeltaRow + }{ + "same": { + r1: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + r2: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + cols: []int{0, 1, 2}, + e: model1.DeltaRow{"", "", ""}, + }, + "empty": { + r1: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + r2: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + e: model1.DeltaRow{}, + }, + "diff-full": { + r1: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + r2: model1.Row{ + Fields: model1.Fields{"a1", "b1", "c1"}, + }, + cols: []int{0, 1, 2}, + e: model1.DeltaRow{"a", "b", "c"}, + }, + "diff-reverse": { + r1: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + r2: model1.Row{ + Fields: model1.Fields{"a1", "b1", "c1"}, + }, + cols: []int{2, 1, 0}, + e: model1.DeltaRow{"c", "b", "a"}, + }, + "diff-skip": { + r1: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + r2: model1.Row{ + Fields: model1.Fields{"a1", "b1", "c1"}, + }, + cols: []int{2, 0}, + e: model1.DeltaRow{"c", "a"}, + }, + "diff-missing": { + r1: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + r2: model1.Row{ + Fields: model1.Fields{"a1", "b1", "c1"}, + }, + cols: []int{2, 10, 0}, + e: model1.DeltaRow{"c", "", "a"}, + }, + "diff-negative": { + r1: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + r2: model1.Row{ + Fields: model1.Fields{"a1", "b1", "c1"}, + }, + cols: []int{2, -1, 0}, + e: model1.DeltaRow{"c", "", "a"}, + }, + } + + hh := model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "C"}, + } + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + d := model1.NewDeltaRow(u.r1, u.r2, hh) + out := make(model1.DeltaRow, len(u.cols)) + d.Customize(u.cols, out) + assert.Equal(t, u.e, out) + }) + } +} + +func TestDeltaNew(t *testing.T) { + uu := map[string]struct { + o model1.Row + n model1.Row + blank bool + e model1.DeltaRow + }{ + "same": { + o: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + n: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + blank: true, + e: model1.DeltaRow{"", "", ""}, + }, + "diff": { + o: model1.Row{ + Fields: model1.Fields{"a1", "b", "c"}, + }, + n: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + e: model1.DeltaRow{"a1", "", ""}, + }, + "diff2": { + o: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + n: model1.Row{ + Fields: model1.Fields{"a", "b1", "c"}, + }, + e: model1.DeltaRow{"", "b", ""}, + }, + "diffLast": { + o: model1.Row{ + Fields: model1.Fields{"a", "b", "c"}, + }, + n: model1.Row{ + Fields: model1.Fields{"a", "b", "c1"}, + }, + e: model1.DeltaRow{"", "", "c"}, + }, + } + + hh := model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "C"}, + } + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + d := model1.NewDeltaRow(u.o, u.n, hh) + assert.Equal(t, u.e, d) + assert.Equal(t, u.blank, d.IsBlank()) + }) + } +} + +func TestDeltaBlank(t *testing.T) { + uu := map[string]struct { + r model1.DeltaRow + e bool + }{ + "empty": { + r: model1.DeltaRow{}, + e: true, + }, + "blank": { + r: model1.DeltaRow{"", "", ""}, + e: true, + }, + "notblank": { + r: model1.DeltaRow{"", "", "z"}, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.r.IsBlank()) + }) + } +} + +func TestDeltaDiff(t *testing.T) { + uu := map[string]struct { + d1, d2 model1.DeltaRow + ageCol int + e bool + }{ + "empty": { + d1: model1.DeltaRow{"f1", "f2", "f3"}, + ageCol: 2, + e: true, + }, + "same": { + d1: model1.DeltaRow{"f1", "f2", "f3"}, + d2: model1.DeltaRow{"f1", "f2", "f3"}, + ageCol: -1, + }, + "diff": { + d1: model1.DeltaRow{"f1", "f2", "f3"}, + d2: model1.DeltaRow{"f1", "f2", "f13"}, + ageCol: -1, + e: true, + }, + "diff-age-first": { + d1: model1.DeltaRow{"f1", "f2", "f3"}, + d2: model1.DeltaRow{"f1", "f2", "f13"}, + ageCol: 0, + e: true, + }, + "diff-age-last": { + d1: model1.DeltaRow{"f1", "f2", "f3"}, + d2: model1.DeltaRow{"f1", "f2", "f13"}, + ageCol: 2, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.d1.Diff(u.d2, u.ageCol)) + }) + } +} diff --git a/internal/model1/fields.go b/internal/model1/fields.go new file mode 100644 index 0000000000..9242d54f3b --- /dev/null +++ b/internal/model1/fields.go @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1 + +import "reflect" + +// Fields represents a collection of row fields. +type Fields []string + +// Customize returns a subset of fields. +func (f Fields) Customize(cols []int, out Fields) { + for i, c := range cols { + if c < 0 { + out[i] = NAValue + continue + } + if c < len(f) { + out[i] = f[c] + } + } +} + +// Diff returns true if fields differ or false otherwise. +func (f Fields) Diff(ff Fields, ageCol int) bool { + if ageCol < 0 { + return !reflect.DeepEqual(f[:len(f)-1], ff[:len(ff)-1]) + } + if !reflect.DeepEqual(f[:ageCol], ff[:ageCol]) { + return true + } + return !reflect.DeepEqual(f[ageCol+1:], ff[ageCol+1:]) +} + +// Clone returns a copy of the fields. +func (f Fields) Clone() Fields { + cp := make(Fields, len(f)) + copy(cp, f) + + return cp +} diff --git a/internal/render/header.go b/internal/model1/header.go similarity index 82% rename from internal/render/header.go rename to internal/model1/header.go index 253189cf29..798f1d92ce 100644 --- a/internal/render/header.go +++ b/internal/model1/header.go @@ -1,4 +1,7 @@ -package render +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1 import ( "reflect" @@ -18,6 +21,7 @@ type HeaderColumn struct { MX bool Time bool Capacity bool + VS bool } // Clone copies a header. @@ -30,18 +34,24 @@ func (h HeaderColumn) Clone() HeaderColumn { // Header represents a table header. type Header []HeaderColumn +func (h Header) Clear() Header { + h = h[:0] + + return h +} + // Clone duplicates a header. func (h Header) Clone() Header { - header := make(Header, len(h)) - for i, c := range h { - header[i] = c.Clone() + he := make(Header, 0, len(h)) + for _, h := range h { + he = append(he, h.Clone()) } - return header + return he } // Labelize returns a new Header based on labels. -func (h Header) Labelize(cols []int, labelCol int, rr RowEvents) Header { +func (h Header) Labelize(cols []int, labelCol int, rr *RowEvents) Header { header := make(Header, 0, len(cols)+1) for _, c := range cols { header = append(header, h[c]) @@ -59,8 +69,8 @@ func (h Header) MapIndices(cols []string, wide bool) []int { ii := make([]int, 0, len(cols)) cc := make(map[int]struct{}, len(cols)) for _, col := range cols { - idx := h.IndexOf(col, true) - if idx < 0 { + idx, ok := h.IndexOf(col, true) + if !ok { log.Warn().Msgf("Column %q not found on resource", col) } ii, cc[idx] = append(ii, idx), struct{}{} @@ -86,13 +96,10 @@ func (h Header) Customize(cols []string, wide bool) Header { cc := make(Header, 0, len(h)) xx := make(map[int]struct{}, len(h)) for _, c := range cols { - idx := h.IndexOf(c, true) - if idx == -1 { + idx, ok := h.IndexOf(c, true) + if !ok { log.Warn().Msgf("Column %s is not available on this resource", c) - col := HeaderColumn{ - Name: c, - } - cc = append(cc, col) + cc = append(cc, HeaderColumn{Name: c}) continue } xx[idx] = struct{}{} @@ -125,8 +132,8 @@ func (h Header) Diff(header Header) bool { return !reflect.DeepEqual(h, header) } -// Columns return header as a collection of strings. -func (h Header) Columns(wide bool) []string { +// ColumnNames return header col names +func (h Header) ColumnNames(wide bool) []string { if len(h) == 0 { return nil } @@ -143,7 +150,9 @@ func (h Header) Columns(wide bool) []string { // HasAge returns true if table has an age column. func (h Header) HasAge() bool { - return h.IndexOf(ageCol, true) != -1 + _, ok := h.IndexOf(ageCol, true) + + return ok } // IsMetricsCol checks if given column index represents metrics. @@ -173,22 +182,17 @@ func (h Header) IsCapacityCol(col int) bool { return h[col].Capacity } -// ValidColIndex returns the valid col index or -1 if none. -func (h Header) ValidColIndex() int { - return h.IndexOf("VALID", true) -} - // IndexOf returns the col index or -1 if none. -func (h Header) IndexOf(colName string, includeWide bool) int { +func (h Header) IndexOf(colName string, includeWide bool) (int, bool) { for i, c := range h { if c.Wide && !includeWide { continue } if c.Name == colName { - return i + return i, true } } - return -1 + return -1, false } // Dump for debugging. diff --git a/internal/render/header_test.go b/internal/model1/header_test.go similarity index 52% rename from internal/render/header_test.go rename to internal/model1/header_test.go index 140e89c890..3d1d62f321 100644 --- a/internal/render/header_test.go +++ b/internal/model1/header_test.go @@ -1,15 +1,18 @@ -package render_test +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1_test import ( "testing" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" "github.com/stretchr/testify/assert" ) func TestHeaderMapIndices(t *testing.T) { uu := map[string]struct { - h1 render.Header + h1 model1.Header cols []string wide bool e []int @@ -47,15 +50,16 @@ func TestHeaderMapIndices(t *testing.T) { func TestHeaderIndexOf(t *testing.T) { uu := map[string]struct { - h render.Header - name string - wide bool - e int + h model1.Header + name string + wide, ok bool + e int }{ "shown": { h: makeHeader(), name: "A", e: 0, + ok: true, }, "hidden": { h: makeHeader(), @@ -67,23 +71,26 @@ func TestHeaderIndexOf(t *testing.T) { name: "B", wide: true, e: 1, + ok: true, }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.h.IndexOf(u.name, u.wide)) + idx, ok := u.h.IndexOf(u.name, u.wide) + assert.Equal(t, u.ok, ok) + assert.Equal(t, u.e, idx) }) } } func TestHeaderCustomize(t *testing.T) { uu := map[string]struct { - h render.Header + h model1.Header cols []string wide bool - e render.Header + e model1.Header }{ "default": { h: makeHeader(), @@ -95,58 +102,58 @@ func TestHeaderCustomize(t *testing.T) { e: makeHeader(), }, "reverse": { - h: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "C"}, + h: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B", Wide: true}, + model1.HeaderColumn{Name: "C"}, }, cols: []string{"C", "A"}, - e: render.Header{ - render.HeaderColumn{Name: "C"}, - render.HeaderColumn{Name: "A"}, + e: model1.Header{ + model1.HeaderColumn{Name: "C"}, + model1.HeaderColumn{Name: "A"}, }, }, "reverse-wide": { - h: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "C"}, + h: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B", Wide: true}, + model1.HeaderColumn{Name: "C"}, }, cols: []string{"C", "A"}, wide: true, - e: render.Header{ - render.HeaderColumn{Name: "C"}, - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, + e: model1.Header{ + model1.HeaderColumn{Name: "C"}, + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B", Wide: true}, }, }, "toggle-wide": { - h: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "C"}, + h: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B", Wide: true}, + model1.HeaderColumn{Name: "C"}, }, cols: []string{"C", "B"}, wide: true, - e: render.Header{ - render.HeaderColumn{Name: "C"}, - render.HeaderColumn{Name: "B", Wide: false}, - render.HeaderColumn{Name: "A", Wide: true}, + e: model1.Header{ + model1.HeaderColumn{Name: "C"}, + model1.HeaderColumn{Name: "B", Wide: false}, + model1.HeaderColumn{Name: "A", Wide: true}, }, }, "missing": { - h: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "C"}, + h: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B", Wide: true}, + model1.HeaderColumn{Name: "C"}, }, cols: []string{"BLEE", "A"}, wide: true, - e: render.Header{ - render.HeaderColumn{Name: "BLEE"}, - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "C", Wide: true}, + e: model1.Header{ + model1.HeaderColumn{Name: "BLEE"}, + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B", Wide: true}, + model1.HeaderColumn{Name: "C", Wide: true}, }, }, } @@ -161,7 +168,7 @@ func TestHeaderCustomize(t *testing.T) { func TestHeaderDiff(t *testing.T) { uu := map[string]struct { - h1, h2 render.Header + h1, h2 model1.Header e bool }{ "same": { @@ -174,37 +181,37 @@ func TestHeaderDiff(t *testing.T) { e: true, }, "differ-wide": { - h1: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "C"}, + h1: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B", Wide: true}, + model1.HeaderColumn{Name: "C"}, }, - h2: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, + h2: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "C"}, }, e: true, }, "differ-order": { - h1: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "C"}, + h1: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B", Wide: true}, + model1.HeaderColumn{Name: "C"}, }, - h2: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "C"}, - render.HeaderColumn{Name: "B", Wide: true}, + h2: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "C"}, + model1.HeaderColumn{Name: "B", Wide: true}, }, e: true, }, "differ-name": { - h1: render.Header{ - render.HeaderColumn{Name: "A"}, + h1: model1.Header{ + model1.HeaderColumn{Name: "A"}, }, - h2: render.Header{ - render.HeaderColumn{Name: "B"}, + h2: model1.Header{ + model1.HeaderColumn{Name: "B"}, }, e: true, }, @@ -220,17 +227,17 @@ func TestHeaderDiff(t *testing.T) { func TestHeaderHasAge(t *testing.T) { uu := map[string]struct { - h render.Header + h model1.Header age, e bool }{ "no-age": { - h: render.Header{}, + h: model1.Header{}, }, "age": { - h: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "AGE", Time: true}, + h: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, }, e: true, age: true, @@ -246,41 +253,14 @@ func TestHeaderHasAge(t *testing.T) { } } -func TestHeaderValidColIndex(t *testing.T) { - uu := map[string]struct { - h render.Header - e int - }{ - "none": { - h: render.Header{}, - e: -1, - }, - "valid": { - h: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "VALID", Wide: true}, - }, - e: 2, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.h.ValidColIndex()) - }) - } -} - func TestHeaderColumns(t *testing.T) { uu := map[string]struct { - h render.Header + h model1.Header wide bool e []string }{ "empty": { - h: render.Header{}, + h: model1.Header{}, }, "regular": { h: makeHeader(), @@ -296,17 +276,17 @@ func TestHeaderColumns(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.h.Columns(u.wide)) + assert.Equal(t, u.e, u.h.ColumnNames(u.wide)) }) } } func TestHeaderClone(t *testing.T) { uu := map[string]struct { - h render.Header + h model1.Header }{ "empty": { - h: render.Header{}, + h: model1.Header{}, }, "full": { h: makeHeader(), @@ -329,10 +309,10 @@ func TestHeaderClone(t *testing.T) { // ---------------------------------------------------------------------------- // Helpers... -func makeHeader() render.Header { - return render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "C"}, +func makeHeader() model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B", Wide: true}, + model1.HeaderColumn{Name: "C"}, } } diff --git a/internal/model1/helpers.go b/internal/model1/helpers.go new file mode 100644 index 0000000000..fb0ac597a7 --- /dev/null +++ b/internal/model1/helpers.go @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1 + +import ( + "fmt" + "math" + "sort" + "strings" + + "github.com/fvbommel/sortorder" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +func Hydrate(ns string, oo []runtime.Object, rr Rows, re Renderer) error { + for i, o := range oo { + if err := re.Render(o, ns, &rr[i]); err != nil { + return err + } + } + + return nil +} + +func GenericHydrate(ns string, table *metav1.Table, rr Rows, re Renderer) error { + gr, ok := re.(Generic) + if !ok { + return fmt.Errorf("expecting generic renderer but got %T", re) + } + gr.SetTable(ns, table) + for i, row := range table.Rows { + if err := gr.Render(row, ns, &rr[i]); err != nil { + return err + } + } + + return nil +} + +// IsValid returns true if resource is valid, false otherwise. +func IsValid(ns string, h Header, r Row) bool { + if len(r.Fields) == 0 { + return true + } + idx, ok := h.IndexOf("VALID", true) + if !ok || idx >= len(r.Fields) { + return true + } + + return strings.TrimSpace(r.Fields[idx]) == "" +} + +func sortLabels(m map[string]string) (keys, vals []string) { + for k := range m { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + vals = append(vals, m[k]) + } + + return +} + +// Converts labels string to map. +func labelize(labels string) map[string]string { + ll := strings.Split(labels, ",") + data := make(map[string]string, len(ll)) + for _, l := range ll { + tokens := strings.Split(l, "=") + if len(tokens) == 2 { + data[tokens[0]] = tokens[1] + } + } + + return data +} + +func durationToSeconds(duration string) int64 { + if len(duration) == 0 { + return 0 + } + if duration == NAValue { + return math.MaxInt64 + } + + num := make([]rune, 0, 5) + var n, m int64 + for _, r := range duration { + switch r { + case 'y': + m = 365 * 24 * 60 * 60 + case 'd': + m = 24 * 60 * 60 + case 'h': + m = 60 * 60 + case 'm': + m = 60 + case 's': + m = 1 + default: + num = append(num, r) + continue + } + n, num = n+runesToNum(num)*m, num[:0] + } + + return n +} + +func runesToNum(rr []rune) int64 { + var r int64 + var m int64 = 1 + for i := len(rr) - 1; i >= 0; i-- { + v := int64(rr[i] - '0') + r += v * m + m *= 10 + } + + return r +} + +func capacityToNumber(capacity string) int64 { + quantity := resource.MustParse(capacity) + return quantity.Value() +} + +// Less return true if c1 <= c2. +func Less(isNumber, isDuration, isCapacity bool, id1, id2, v1, v2 string) bool { + var less bool + switch { + case isNumber: + less = lessNumber(v1, v2) + case isDuration: + less = lessDuration(v1, v2) + case isCapacity: + less = lessCapacity(v1, v2) + default: + less = sortorder.NaturalLess(v1, v2) + } + if v1 == v2 { + return sortorder.NaturalLess(id1, id2) + } + + return less +} + +func lessDuration(s1, s2 string) bool { + d1, d2 := durationToSeconds(s1), durationToSeconds(s2) + return d1 <= d2 +} + +func lessCapacity(s1, s2 string) bool { + c1, c2 := capacityToNumber(s1), capacityToNumber(s2) + + return c1 <= c2 +} + +func lessNumber(s1, s2 string) bool { + v1, v2 := strings.Replace(s1, ",", "", -1), strings.Replace(s2, ",", "", -1) + + return sortorder.NaturalLess(v1, v2) +} diff --git a/internal/model1/helpers_test.go b/internal/model1/helpers_test.go new file mode 100644 index 0000000000..3e6a04272f --- /dev/null +++ b/internal/model1/helpers_test.go @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1 + +import ( + "math" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSortLabels(t *testing.T) { + uu := map[string]struct { + labels string + e [][]string + }{ + "simple": { + labels: "a=b,c=d", + e: [][]string{ + {"a", "c"}, + {"b", "d"}, + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + hh, vv := sortLabels(labelize(u.labels)) + assert.Equal(t, u.e[0], hh) + assert.Equal(t, u.e[1], vv) + }) + } +} + +func TestLabelize(t *testing.T) { + uu := map[string]struct { + labels string + e map[string]string + }{ + "simple": { + labels: "a=b,c=d", + e: map[string]string{"a": "b", "c": "d"}, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, labelize(u.labels)) + }) + } +} + +func TestDurationToSecond(t *testing.T) { + uu := map[string]struct { + s string + e int64 + }{ + "seconds": {s: "22s", e: 22}, + "minutes": {s: "22m", e: 1320}, + "hours": {s: "12h", e: 43200}, + "days": {s: "3d", e: 259200}, + "day_hour": {s: "3d9h", e: 291600}, + "day_hour_minute": {s: "2d22h3m", e: 252180}, + "day_hour_minute_seconds": {s: "2d22h3m50s", e: 252230}, + "year": {s: "3y", e: 94608000}, + "year_day": {s: "1y2d", e: 31708800}, + "n/a": {s: NAValue, e: math.MaxInt64}, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, durationToSeconds(u.s)) + }) + } +} + +func BenchmarkDurationToSecond(b *testing.B) { + t := "2d22h3m50s" + + b.ReportAllocs() + b.ResetTimer() + for n := 0; n < b.N; n++ { + durationToSeconds(t) + } +} diff --git a/internal/model1/row.go b/internal/model1/row.go new file mode 100644 index 0000000000..bda6a86065 --- /dev/null +++ b/internal/model1/row.go @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1 + +// Row represents a collection of columns. +type Row struct { + ID string + Fields Fields +} + +// NewRow returns a new row with initialized fields. +func NewRow(size int) Row { + return Row{Fields: make([]string, size)} +} + +// Labelize returns a new row based on labels. +func (r Row) Labelize(cols []int, labelCol int, labels []string) Row { + out := NewRow(len(cols) + len(labels)) + for _, col := range cols { + out.Fields = append(out.Fields, r.Fields[col]) + } + m := labelize(r.Fields[labelCol]) + for _, label := range labels { + out.Fields = append(out.Fields, m[label]) + } + + return out +} + +// Customize returns a row subset based on given col indices. +func (r Row) Customize(cols []int) Row { + out := NewRow(len(cols)) + r.Fields.Customize(cols, out.Fields) + out.ID = r.ID + + return out +} + +// Diff returns true if row differ or false otherwise. +func (r Row) Diff(ro Row, ageCol int) bool { + if r.ID != ro.ID { + return true + } + return r.Fields.Diff(ro.Fields, ageCol) +} + +// Clone copies a row. +func (r Row) Clone() Row { + return Row{ + ID: r.ID, + Fields: r.Fields.Clone(), + } +} + +// Len returns the length of the row. +func (r Row) Len() int { + return len(r.Fields) +} + +// ---------------------------------------------------------------------------- + +// RowSorter sorts rows. +type RowSorter struct { + Rows Rows + Index int + IsNumber bool + IsDuration bool + IsCapacity bool + Asc bool +} + +func (s RowSorter) Len() int { + return len(s.Rows) +} + +func (s RowSorter) Swap(i, j int) { + s.Rows[i], s.Rows[j] = s.Rows[j], s.Rows[i] +} + +func (s RowSorter) Less(i, j int) bool { + v1, v2 := s.Rows[i].Fields[s.Index], s.Rows[j].Fields[s.Index] + id1, id2 := s.Rows[i].ID, s.Rows[j].ID + less := Less(s.IsNumber, s.IsDuration, s.IsCapacity, id1, id2, v1, v2) + if s.Asc { + return less + } + return !less +} + +// ---------------------------------------------------------------------------- +// Helpers... diff --git a/internal/render/row_event.go b/internal/model1/row_event.go similarity index 53% rename from internal/render/row_event.go rename to internal/model1/row_event.go index 83c7c44022..628ddab057 100644 --- a/internal/render/row_event.go +++ b/internal/model1/row_event.go @@ -1,25 +1,14 @@ -package render +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1 import ( + "fmt" "sort" ) -const ( - // EventUnchanged notifies listener resource has not changed. - EventUnchanged ResEvent = 1 << iota - - // EventAdd notifies listener of a resource was added. - EventAdd - - // EventUpdate notifies listener of a resource updated. - EventUpdate - - // EventDelete notifies listener of a resource was deleted. - EventDelete - - // EventClear the stack was reset. - EventClear -) +type ReRangeFn func(int, RowEvent) bool // ResEvent represents a resource event. type ResEvent int @@ -100,13 +89,58 @@ func (r RowEvent) Diff(re RowEvent, ageCol int) bool { // ---------------------------------------------------------------------------- +type reIndex map[string]int + // RowEvents a collection of row events. -type RowEvents []RowEvent +type RowEvents struct { + events []RowEvent + index reIndex +} + +func NewRowEvents(size int) *RowEvents { + return &RowEvents{ + events: make([]RowEvent, 0, size), + index: make(reIndex, size), + } +} + +func NewRowEventsWithEvts(ee ...RowEvent) *RowEvents { + re := NewRowEvents(len(ee)) + for _, e := range ee { + re.Add(e) + } + + return re +} + +func (r *RowEvents) reindex() { + for i, e := range r.events { + r.index[e.Row.ID] = i + } +} + +func (r *RowEvents) At(i int) (RowEvent, bool) { + if i < 0 || i > len(r.events) { + return RowEvent{}, false + } + + return r.events[i], true +} + +func (r *RowEvents) Set(i int, re RowEvent) { + r.events[i] = re + r.index[re.Row.ID] = i +} + +func (r *RowEvents) Add(re RowEvent) { + r.events = append(r.events, re) + r.index[re.Row.ID] = len(r.events) - 1 +} // ExtractHeaderLabels extract header labels. -func (r RowEvents) ExtractHeaderLabels(labelCol int) []string { +func (r *RowEvents) ExtractHeaderLabels(labelCol int) []string { ll := make([]string, 0, 10) - for _, re := range r { + for _, re := range r.events { ll = append(ll, re.ExtractHeaderLabels(labelCol)...) } @@ -114,32 +148,32 @@ func (r RowEvents) ExtractHeaderLabels(labelCol int) []string { } // Labelize converts labels into a row event. -func (r RowEvents) Labelize(cols []int, labelCol int, labels []string) RowEvents { - out := make(RowEvents, 0, len(r)) - for _, re := range r { +func (r *RowEvents) Labelize(cols []int, labelCol int, labels []string) *RowEvents { + out := make([]RowEvent, 0, len(r.events)) + for _, re := range r.events { out = append(out, re.Labelize(cols, labelCol, labels)) } - return out + return NewRowEventsWithEvts(out...) } // Customize returns custom row events based on columns layout. -func (r RowEvents) Customize(cols []int) RowEvents { - ee := make(RowEvents, 0, len(cols)) - for _, re := range r { +func (r *RowEvents) Customize(cols []int) *RowEvents { + ee := make([]RowEvent, 0, len(cols)) + for _, re := range r.events { ee = append(ee, re.Customize(cols)) } - return ee + + return NewRowEventsWithEvts(ee...) } // Diff returns true if the event changed. -func (r RowEvents) Diff(re RowEvents, ageCol int) bool { - if len(r) != len(re) { +func (r *RowEvents) Diff(re *RowEvents, ageCol int) bool { + if len(r.events) != len(re.events) { return true } - - for i := range r { - if r[i].Diff(re[i], ageCol) { + for i := range r.events { + if r.events[i].Diff(re.events[i], ageCol) { return true } } @@ -147,53 +181,80 @@ func (r RowEvents) Diff(re RowEvents, ageCol int) bool { return false } -// Clone returns a rowevents deep copy. -func (r RowEvents) Clone() RowEvents { - res := make(RowEvents, len(r)) - for i, re := range r { - res[i] = re.Clone() +// Clone returns a deep copy. +func (r *RowEvents) Clone() *RowEvents { + re := make([]RowEvent, 0, len(r.events)) + for _, e := range r.events { + re = append(re, e.Clone()) } - return res + return NewRowEventsWithEvts(re...) } // Upsert add or update a row if it exists. -func (r RowEvents) Upsert(re RowEvent) RowEvents { +func (r *RowEvents) Upsert(re RowEvent) { if idx, ok := r.FindIndex(re.Row.ID); ok { - r[idx] = re + r.events[idx] = re } else { - r = append(r, re) + r.Add(re) } - return r } // Delete removes an element by id. -func (r RowEvents) Delete(id string) RowEvents { - victim, ok := r.FindIndex(id) +func (r *RowEvents) Delete(fqn string) error { + victim, ok := r.FindIndex(fqn) if !ok { - return r + return fmt.Errorf("unable to delete row with fqn: %q", fqn) } - return append(r[0:victim], r[victim+1:]...) + r.events = append(r.events[0:victim], r.events[victim+1:]...) + delete(r.index, fqn) + r.reindex() + + return nil +} + +func (r *RowEvents) Len() int { + return len(r.events) +} + +func (r *RowEvents) Empty() bool { + return len(r.events) == 0 } // Clear delete all row events. -func (r RowEvents) Clear() RowEvents { - return RowEvents{} +func (r *RowEvents) Clear() { + r.events = r.events[:0] + for k := range r.index { + delete(r.index, k) + } } -// FindIndex locates a row index by id. Returns false is not found. -func (r RowEvents) FindIndex(id string) (int, bool) { - for i, re := range r { - if re.Row.ID == id { - return i, true +func (r *RowEvents) Range(f ReRangeFn) { + for i, e := range r.events { + if !f(i, e) { + return } } +} + +func (r *RowEvents) Get(id string) (RowEvent, bool) { + i, ok := r.index[id] + if !ok { + return RowEvent{}, false + } - return 0, false + return r.At(i) +} + +// FindIndex locates a row index by id. Returns false is not found. +func (r *RowEvents) FindIndex(id string) (int, bool) { + i, ok := r.index[id] + + return i, ok } // Sort rows based on column index and order. -func (r RowEvents) Sort(ns string, sortCol int, isDuration, numCol, isCapacity, asc bool) { +func (r *RowEvents) Sort(ns string, sortCol int, isDuration, numCol, isCapacity, asc bool) { if sortCol == -1 { return } @@ -208,13 +269,14 @@ func (r RowEvents) Sort(ns string, sortCol int, isDuration, numCol, isCapacity, IsCapacity: isCapacity, } sort.Sort(t) + r.reindex() } // ---------------------------------------------------------------------------- // RowEventSorter sorts row events by a given colon. type RowEventSorter struct { - Events RowEvents + Events *RowEvents Index int NS string IsNumber bool @@ -224,16 +286,16 @@ type RowEventSorter struct { } func (r RowEventSorter) Len() int { - return len(r.Events) + return len(r.Events.events) } func (r RowEventSorter) Swap(i, j int) { - r.Events[i], r.Events[j] = r.Events[j], r.Events[i] + r.Events.events[i], r.Events.events[j] = r.Events.events[j], r.Events.events[i] } func (r RowEventSorter) Less(i, j int) bool { - f1, f2 := r.Events[i].Row.Fields, r.Events[j].Row.Fields - id1, id2 := r.Events[i].Row.ID, r.Events[j].Row.ID + f1, f2 := r.Events.events[i].Row.Fields, r.Events.events[j].Row.Fields + id1, id2 := r.Events.events[i].Row.ID, r.Events.events[j].Row.ID less := Less(r.IsNumber, r.IsDuration, r.IsCapacity, id1, id2, f1[r.Index], f2[r.Index]) if r.Asc { return less @@ -241,50 +303,3 @@ func (r RowEventSorter) Less(i, j int) bool { return !less } - -// ---------------------------------------------------------------------------- - -// // IdSorter sorts row events by a given id. -// type IdSorter struct { -// Ids map[string]int -// Events RowEvents -// } - -// func (s IdSorter) Len() int { -// return len(s.Events) -// } - -// func (s IdSorter) Swap(i, j int) { -// s.Events[i], s.Events[j] = s.Events[j], s.Events[i] -// } - -// func (s IdSorter) Less(i, j int) bool { -// return s.Ids[s.Events[i].Row.ID] < s.Ids[s.Events[j].Row.ID] -// } - -// ---------------------------------------------------------------------------- - -// // StringSet represents a collection of unique strings. -// type StringSet []string - -// // Add adds a new item in the set. -// func (ss StringSet) Add(item string) StringSet { -// if ss.In(item) { -// return ss -// } -// return append(ss, item) -// } - -// // In checks if a string is in the set. -// func (ss StringSet) In(item string) bool { -// return ss.indexOf(item) >= 0 -// } - -// func (ss StringSet) indexOf(item string) int { -// for i, s := range ss { -// if s == item { -// return i -// } -// } -// return -1 -// } diff --git a/internal/model1/row_event_test.go b/internal/model1/row_event_test.go new file mode 100644 index 0000000000..9ca9322980 --- /dev/null +++ b/internal/model1/row_event_test.go @@ -0,0 +1,543 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1_test + +import ( + "testing" + "time" + + "github.com/derailed/k9s/internal/model1" + "github.com/stretchr/testify/assert" +) + +func TestRowEventCustomize(t *testing.T) { + uu := map[string]struct { + re1, e model1.RowEvent + cols []int + }{ + "empty": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + e: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{}}, + }, + }, + "full": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + e: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + cols: []int{0, 1, 2}, + }, + "deltas": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + Deltas: model1.DeltaRow{"a", "b", "c"}, + }, + e: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + Deltas: model1.DeltaRow{"a", "b", "c"}, + }, + cols: []int{0, 1, 2}, + }, + "deltas-skip": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + Deltas: model1.DeltaRow{"a", "b", "c"}, + }, + e: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"3", "1"}}, + Deltas: model1.DeltaRow{"c", "a"}, + }, + cols: []int{2, 0}, + }, + "reverse": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + e: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"3", "2", "1"}}, + }, + cols: []int{2, 1, 0}, + }, + "skip": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + e: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"3", "1"}}, + }, + cols: []int{2, 0}, + }, + "miss": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + e: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"3", "", "1"}}, + }, + cols: []int{2, 10, 0}, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.re1.Customize(u.cols)) + }) + } +} + +func TestRowEventDiff(t *testing.T) { + uu := map[string]struct { + re1, re2 model1.RowEvent + e bool + }{ + "same": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + re2: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + }, + "diff-kind": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + re2: model1.RowEvent{ + Kind: model1.EventDelete, + Row: model1.Row{ID: "B", Fields: model1.Fields{"1", "2", "3"}}, + }, + e: true, + }, + "diff-delta": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + Deltas: model1.DeltaRow{"1", "2", "3"}, + }, + re2: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + Deltas: model1.DeltaRow{"10", "2", "3"}, + }, + e: true, + }, + "diff-id": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + re2: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "B", Fields: model1.Fields{"1", "2", "3"}}, + }, + e: true, + }, + "diff-field": { + re1: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}, + }, + re2: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ID: "A", Fields: model1.Fields{"10", "2", "3"}}, + }, + e: true, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.re1.Diff(u.re2, -1)) + }) + } +} + +func TestRowEventsDiff(t *testing.T) { + uu := map[string]struct { + re1, re2 *model1.RowEvents + ageCol int + e bool + }{ + "same": { + re1: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + re2: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + ageCol: -1, + }, + "diff-len": { + re1: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + re2: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + ageCol: -1, + e: true, + }, + "diff-id": { + re1: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + re2: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "D", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + ageCol: -1, + e: true, + }, + "diff-order": { + re1: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + re2: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + ageCol: -1, + e: true, + }, + "diff-withAge": { + re1: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + re2: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "13"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + ageCol: 1, + e: true, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.re1.Diff(u.re2, u.ageCol)) + }) + } +} + +func TestRowEventsUpsert(t *testing.T) { + uu := map[string]struct { + ee, e *model1.RowEvents + re model1.RowEvent + }{ + "add": { + ee: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + re: model1.RowEvent{ + Row: model1.Row{ID: "D", Fields: model1.Fields{"f1", "f2", "f3"}}, + }, + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "D", Fields: model1.Fields{"f1", "f2", "f3"}}}, + ), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + u.ee.Upsert(u.re) + assert.Equal(t, u.e, u.ee) + }) + } +} + +func TestRowEventsCustomize(t *testing.T) { + uu := map[string]struct { + re, e *model1.RowEvents + cols []int + }{ + "same": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + cols: []int{0, 1, 2}, + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + }, + "reverse": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + cols: []int{2, 1, 0}, + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"3", "2", "1"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"3", "2", "0"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"3", "2", "10"}}}, + ), + }, + "skip": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + cols: []int{1, 0}, + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"2", "1"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"2", "0"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"2", "10"}}}, + ), + }, + "missing": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + cols: []int{1, 0, 4}, + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"2", "1", ""}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"2", "0", ""}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"2", "10", ""}}}, + ), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.re.Customize(u.cols)) + }) + } +} + +func TestRowEventsDelete(t *testing.T) { + uu := map[string]struct { + re, e *model1.RowEvents + id string + }{ + "first": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + id: "A", + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + }, + "middle": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + id: "B", + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + }, + "last": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + id: "C", + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + ), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.NoError(t, u.re.Delete(u.id)) + assert.Equal(t, u.e, u.re) + }) + } +} + +func TestRowEventsSort(t *testing.T) { + uu := map[string]struct { + re, e *model1.RowEvents + col int + duration, num, asc bool + capacity bool + }{ + "age_time": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", testTime().Add(20 * time.Second).String()}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", testTime().Add(10 * time.Second).String()}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", testTime().String()}}}, + ), + col: 2, + asc: true, + duration: true, + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", testTime().String()}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", testTime().Add(10 * time.Second).String()}}}, + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", testTime().Add(20 * time.Second).String()}}}, + ), + }, + "col0": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + col: 0, + asc: true, + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "B", Fields: model1.Fields{"0", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "A", Fields: model1.Fields{"1", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "C", Fields: model1.Fields{"10", "2", "3"}}}, + ), + }, + "id_preserve": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "ns1/B", Fields: model1.Fields{"B", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/A", Fields: model1.Fields{"A", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/C", Fields: model1.Fields{"C", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/B", Fields: model1.Fields{"B", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/A", Fields: model1.Fields{"A", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/C", Fields: model1.Fields{"C", "2", "3"}}}, + ), + col: 1, + asc: true, + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "ns1/A", Fields: model1.Fields{"A", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/B", Fields: model1.Fields{"B", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/C", Fields: model1.Fields{"C", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/A", Fields: model1.Fields{"A", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/B", Fields: model1.Fields{"B", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/C", Fields: model1.Fields{"C", "2", "3"}}}, + ), + }, + "capacity": { + re: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "ns1/B", Fields: model1.Fields{"B", "2", "3", "1Gi"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/A", Fields: model1.Fields{"A", "2", "3", "1.1G"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/C", Fields: model1.Fields{"C", "2", "3", "0.5Ti"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/B", Fields: model1.Fields{"B", "2", "3", "12e6"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/A", Fields: model1.Fields{"A", "2", "3", "1234"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/C", Fields: model1.Fields{"C", "2", "3", "0.1Ei"}}}, + ), + col: 3, + asc: true, + capacity: true, + e: model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "ns2/A", Fields: model1.Fields{"A", "2", "3", "1234"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/B", Fields: model1.Fields{"B", "2", "3", "12e6"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/B", Fields: model1.Fields{"B", "2", "3", "1Gi"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/A", Fields: model1.Fields{"A", "2", "3", "1.1G"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/C", Fields: model1.Fields{"C", "2", "3", "0.5Ti"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/C", Fields: model1.Fields{"C", "2", "3", "0.1Ei"}}}, + ), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + u.re.Sort("", u.col, u.duration, u.num, u.capacity, u.asc) + assert.Equal(t, u.e, u.re) + }) + } +} + +func TestRowEventsClone(t *testing.T) { + uu := map[string]struct { + r *model1.RowEvents + }{ + "empty": { + r: model1.NewRowEventsWithEvts(), + }, + "full": { + r: makeRowEvents(), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + c := u.r.Clone() + assert.Equal(t, u.r.Len(), c.Len()) + if !u.r.Empty() { + r, ok := u.r.At(0) + assert.True(t, ok) + r.Row.Fields[0] = "blee" + cr, ok := c.At(0) + assert.True(t, ok) + assert.Equal(t, "A", cr.Row.Fields[0]) + } + }) + } +} + +// Helpers... + +func makeRowEvents() *model1.RowEvents { + return model1.NewRowEventsWithEvts( + model1.RowEvent{Row: model1.Row{ID: "ns1/A", Fields: model1.Fields{"A", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/B", Fields: model1.Fields{"B", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns1/C", Fields: model1.Fields{"C", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/A", Fields: model1.Fields{"A", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/B", Fields: model1.Fields{"B", "2", "3"}}}, + model1.RowEvent{Row: model1.Row{ID: "ns2/C", Fields: model1.Fields{"C", "2", "3"}}}, + ) +} diff --git a/internal/render/row_test.go b/internal/model1/row_test.go similarity index 77% rename from internal/render/row_test.go rename to internal/model1/row_test.go index 82e4728f44..d206083165 100644 --- a/internal/render/row_test.go +++ b/internal/model1/row_test.go @@ -1,4 +1,7 @@ -package render_test +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1_test import ( "fmt" @@ -6,12 +9,12 @@ import ( "testing" "time" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" "github.com/stretchr/testify/assert" ) func BenchmarkRowCustomize(b *testing.B) { - row := render.Row{ID: "fred", Fields: render.Fields{"f1", "f2", "f3"}} + row := model1.Row{ID: "fred", Fields: model1.Fields{"f1", "f2", "f3"}} cols := []int{0, 1, 2} b.ReportAllocs() b.ResetTimer() @@ -22,36 +25,36 @@ func BenchmarkRowCustomize(b *testing.B) { func TestFieldCustomize(t *testing.T) { uu := map[string]struct { - fields render.Fields + fields model1.Fields cols []int - e render.Fields + e model1.Fields }{ "empty": { - fields: render.Fields{}, + fields: model1.Fields{}, cols: []int{0, 1, 2}, - e: render.Fields{"", "", ""}, + e: model1.Fields{"", "", ""}, }, "no-cols": { - fields: render.Fields{"f1", "f2", "f3"}, + fields: model1.Fields{"f1", "f2", "f3"}, cols: []int{}, - e: render.Fields{}, + e: model1.Fields{}, }, "reverse": { - fields: render.Fields{"f1", "f2", "f3"}, + fields: model1.Fields{"f1", "f2", "f3"}, cols: []int{1, 0}, - e: render.Fields{"f2", "f1"}, + e: model1.Fields{"f2", "f1"}, }, "missing": { - fields: render.Fields{"f1", "f2", "f3"}, + fields: model1.Fields{"f1", "f2", "f3"}, cols: []int{10, 0}, - e: render.Fields{"", "f1"}, + e: model1.Fields{"", "f1"}, }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - ff := make(render.Fields, len(u.cols)) + ff := make(model1.Fields, len(u.cols)) u.fields.Customize(u.cols, ff) assert.Equal(t, u.e, ff) }) @@ -59,7 +62,7 @@ func TestFieldCustomize(t *testing.T) { } func TestFieldClone(t *testing.T) { - f := render.Fields{"a", "b", "c"} + f := model1.Fields{"a", "b", "c"} f1 := f.Clone() assert.True(t, reflect.DeepEqual(f, f1)) @@ -68,24 +71,24 @@ func TestFieldClone(t *testing.T) { func TestRowlabelize(t *testing.T) { uu := map[string]struct { - row render.Row + row model1.Row cols []int - e render.Row + e model1.Row }{ "empty": { - row: render.Row{}, + row: model1.Row{}, cols: []int{0, 1, 2}, - e: render.Row{ID: "", Fields: render.Fields{"", "", ""}}, + e: model1.Row{ID: "", Fields: model1.Fields{"", "", ""}}, }, "no-cols-no-data": { - row: render.Row{}, + row: model1.Row{}, cols: []int{}, - e: render.Row{ID: "", Fields: render.Fields{}}, + e: model1.Row{ID: "", Fields: model1.Fields{}}, }, "no-cols-data": { - row: render.Row{ID: "fred", Fields: render.Fields{"f1", "f2", "f3"}}, + row: model1.Row{ID: "fred", Fields: model1.Fields{"f1", "f2", "f3"}}, cols: []int{}, - e: render.Row{ID: "fred", Fields: render.Fields{}}, + e: model1.Row{ID: "fred", Fields: model1.Fields{}}, }, } @@ -100,24 +103,24 @@ func TestRowlabelize(t *testing.T) { func TestRowCustomize(t *testing.T) { uu := map[string]struct { - row render.Row + row model1.Row cols []int - e render.Row + e model1.Row }{ "empty": { - row: render.Row{}, + row: model1.Row{}, cols: []int{0, 1, 2}, - e: render.Row{ID: "", Fields: render.Fields{"", "", ""}}, + e: model1.Row{ID: "", Fields: model1.Fields{"", "", ""}}, }, "no-cols-no-data": { - row: render.Row{}, + row: model1.Row{}, cols: []int{}, - e: render.Row{ID: "", Fields: render.Fields{}}, + e: model1.Row{ID: "", Fields: model1.Fields{}}, }, "no-cols-data": { - row: render.Row{ID: "fred", Fields: render.Fields{"f1", "f2", "f3"}}, + row: model1.Row{ID: "fred", Fields: model1.Fields{"f1", "f2", "f3"}}, cols: []int{}, - e: render.Row{ID: "fred", Fields: render.Fields{}}, + e: model1.Row{ID: "fred", Fields: model1.Fields{}}, }, } @@ -132,49 +135,49 @@ func TestRowCustomize(t *testing.T) { func TestRowsDelete(t *testing.T) { uu := map[string]struct { - rows render.Rows + rows model1.Rows id string - e render.Rows + e model1.Rows }{ "first": { - rows: render.Rows{ + rows: model1.Rows{ {ID: "a", Fields: []string{"blee", "duh"}}, {ID: "b", Fields: []string{"albert", "blee"}}, }, id: "a", - e: render.Rows{ + e: model1.Rows{ {ID: "b", Fields: []string{"albert", "blee"}}, }, }, "last": { - rows: render.Rows{ + rows: model1.Rows{ {ID: "a", Fields: []string{"blee", "duh"}}, {ID: "b", Fields: []string{"albert", "blee"}}, }, id: "b", - e: render.Rows{ + e: model1.Rows{ {ID: "a", Fields: []string{"blee", "duh"}}, }, }, "middle": { - rows: render.Rows{ + rows: model1.Rows{ {ID: "a", Fields: []string{"blee", "duh"}}, {ID: "b", Fields: []string{"albert", "blee"}}, {ID: "c", Fields: []string{"fred", "zorg"}}, }, id: "b", - e: render.Rows{ + e: model1.Rows{ {ID: "a", Fields: []string{"blee", "duh"}}, {ID: "c", Fields: []string{"fred", "zorg"}}, }, }, "missing": { - rows: render.Rows{ + rows: model1.Rows{ {ID: "a", Fields: []string{"blee", "duh"}}, {ID: "b", Fields: []string{"albert", "blee"}}, }, id: "zorg", - e: render.Rows{ + e: model1.Rows{ {ID: "a", Fields: []string{"blee", "duh"}}, {ID: "b", Fields: []string{"albert", "blee"}}, }, @@ -192,29 +195,29 @@ func TestRowsDelete(t *testing.T) { func TestRowsUpsert(t *testing.T) { uu := map[string]struct { - rows render.Rows - row render.Row - e render.Rows + rows model1.Rows + row model1.Row + e model1.Rows }{ "add": { - rows: render.Rows{ + rows: model1.Rows{ {ID: "a", Fields: []string{"blee", "duh"}}, {ID: "b", Fields: []string{"albert", "blee"}}, }, - row: render.Row{ID: "c", Fields: []string{"f1", "f2"}}, - e: render.Rows{ + row: model1.Row{ID: "c", Fields: []string{"f1", "f2"}}, + e: model1.Rows{ {ID: "a", Fields: []string{"blee", "duh"}}, {ID: "b", Fields: []string{"albert", "blee"}}, {ID: "c", Fields: []string{"f1", "f2"}}, }, }, "update": { - rows: render.Rows{ + rows: model1.Rows{ {ID: "a", Fields: []string{"blee", "duh"}}, {ID: "b", Fields: []string{"albert", "blee"}}, }, - row: render.Row{ID: "a", Fields: []string{"f1", "f2"}}, - e: render.Rows{ + row: model1.Row{ID: "a", Fields: []string{"f1", "f2"}}, + e: model1.Rows{ {ID: "a", Fields: []string{"f1", "f2"}}, {ID: "b", Fields: []string{"albert", "blee"}}, }, @@ -232,69 +235,69 @@ func TestRowsUpsert(t *testing.T) { func TestRowsSortText(t *testing.T) { uu := map[string]struct { - rows render.Rows + rows model1.Rows col int asc, num bool - e render.Rows + e model1.Rows }{ "plainAsc": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{"blee", "duh"}}, {Fields: []string{"albert", "blee"}}, }, col: 0, asc: true, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{"albert", "blee"}}, {Fields: []string{"blee", "duh"}}, }, }, "plainDesc": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{"blee", "duh"}}, {Fields: []string{"albert", "blee"}}, }, col: 0, asc: false, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{"blee", "duh"}}, {Fields: []string{"albert", "blee"}}, }, }, "numericAsc": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{"10", "duh"}}, {Fields: []string{"1", "blee"}}, }, col: 0, num: true, asc: true, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{"1", "blee"}}, {Fields: []string{"10", "duh"}}, }, }, "numericDesc": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{"10", "duh"}}, {Fields: []string{"1", "blee"}}, }, col: 0, num: true, asc: false, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{"10", "duh"}}, {Fields: []string{"1", "blee"}}, }, }, "composite": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{"blee-duh", "duh"}}, {Fields: []string{"blee", "blee"}}, }, col: 0, asc: true, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{"blee", "blee"}}, {Fields: []string{"blee-duh", "duh"}}, }, @@ -312,54 +315,54 @@ func TestRowsSortText(t *testing.T) { func TestRowsSortDuration(t *testing.T) { uu := map[string]struct { - rows render.Rows + rows model1.Rows col int asc bool - e render.Rows + e model1.Rows }{ "fred": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{"2m24s", "blee"}}, {Fields: []string{"2m12s", "duh"}}, }, col: 0, asc: true, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{"2m12s", "duh"}}, {Fields: []string{"2m24s", "blee"}}, }, }, "years": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{testTime().Add(-365 * 24 * time.Hour).String(), "blee"}}, {Fields: []string{testTime().String(), "duh"}}, }, col: 0, asc: true, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{testTime().String(), "duh"}}, {Fields: []string{testTime().Add(-365 * 24 * time.Hour).String(), "blee"}}, }, }, "durationAsc": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{testTime().Add(10 * time.Second).String(), "duh"}}, {Fields: []string{testTime().String(), "blee"}}, }, col: 0, asc: true, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{testTime().String(), "blee"}}, {Fields: []string{testTime().Add(10 * time.Second).String(), "duh"}}, }, }, "durationDesc": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{testTime().Add(10 * time.Second).String(), "duh"}}, {Fields: []string{testTime().String(), "blee"}}, }, col: 0, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{testTime().Add(10 * time.Second).String(), "duh"}}, {Fields: []string{testTime().String(), "blee"}}, }, @@ -377,31 +380,31 @@ func TestRowsSortDuration(t *testing.T) { func TestRowsSortMetrics(t *testing.T) { uu := map[string]struct { - rows render.Rows + rows model1.Rows col int asc bool - e render.Rows + e model1.Rows }{ "metricAsc": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{"10m", "duh"}}, {Fields: []string{"1m", "blee"}}, }, col: 0, asc: true, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{"1m", "blee"}}, {Fields: []string{"10m", "duh"}}, }, }, "metricDesc": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{"10000m", "1000Mi"}}, {Fields: []string{"1m", "50Mi"}}, }, col: 1, asc: false, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{"10000m", "1000Mi"}}, {Fields: []string{"1m", "50Mi"}}, }, @@ -419,31 +422,31 @@ func TestRowsSortMetrics(t *testing.T) { func TestRowsSortCapacity(t *testing.T) { uu := map[string]struct { - rows render.Rows + rows model1.Rows col int asc bool - e render.Rows + e model1.Rows }{ "capacityAsc": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{"10Gi", "duh"}}, {Fields: []string{"10G", "blee"}}, }, col: 0, asc: true, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{"10G", "blee"}}, {Fields: []string{"10Gi", "duh"}}, }, }, "capacityDesc": { - rows: render.Rows{ + rows: model1.Rows{ {Fields: []string{"10000m", "1000Mi"}}, {Fields: []string{"1m", "50Mi"}}, }, col: 1, asc: false, - e: render.Rows{ + e: model1.Rows{ {Fields: []string{"10000m", "1000Mi"}}, {Fields: []string{"1m", "50Mi"}}, }, @@ -511,7 +514,7 @@ func TestLess(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, render.Less(u.isNumber, u.isDuration, u.isCapacity, u.id1, u.id2, u.v1, u.v2)) + assert.Equal(t, u.e, model1.Less(u.isNumber, u.isDuration, u.isCapacity, u.id1, u.id2, u.v1, u.v2)) }) } } diff --git a/internal/model1/rows.go b/internal/model1/rows.go new file mode 100644 index 0000000000..4085cb9b08 --- /dev/null +++ b/internal/model1/rows.go @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1 + +import "sort" + +// Rows represents a collection of rows. +type Rows []Row + +// Delete removes an element by id. +func (rr Rows) Delete(id string) Rows { + idx, ok := rr.Find(id) + if !ok { + return rr + } + + if idx == 0 { + return rr[1:] + } + if idx+1 == len(rr) { + return rr[:len(rr)-1] + } + + return append(rr[:idx], rr[idx+1:]...) +} + +// Upsert adds a new item. +func (rr Rows) Upsert(r Row) Rows { + idx, ok := rr.Find(r.ID) + if !ok { + return append(rr, r) + } + rr[idx] = r + + return rr +} + +// Find locates a row by id. Returns false is not found. +func (rr Rows) Find(id string) (int, bool) { + for i, r := range rr { + if r.ID == id { + return i, true + } + } + + return 0, false +} + +// Sort rows based on column index and order. +func (rr Rows) Sort(col int, asc, isNum, isDur, isCapacity bool) { + t := RowSorter{ + Rows: rr, + Index: col, + IsNumber: isNum, + IsDuration: isDur, + IsCapacity: isCapacity, + Asc: asc, + } + sort.Sort(t) +} diff --git a/internal/model1/table_data.go b/internal/model1/table_data.go new file mode 100644 index 0000000000..13ef48ce81 --- /dev/null +++ b/internal/model1/table_data.go @@ -0,0 +1,497 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1 + +import ( + "context" + "errors" + "fmt" + "regexp" + "strings" + "sync" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config" + "github.com/rs/zerolog/log" + "github.com/sahilm/fuzzy" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +type ( + // SortFn represent a function that can sort columnar data. + SortFn func(rows Rows, sortCol SortColumn) + + // SortColumn represents a sortable column. + SortColumn struct { + Name string + ASC bool + } +) + +const spacer = " " + +type FilterOpts struct { + Toast bool + Filter string + Invert bool +} + +// TableData tracks a K8s resource for tabular display. +type TableData struct { + header Header + rowEvents *RowEvents + namespace string + gvr client.GVR + mx sync.RWMutex +} + +// NewTableData returns a new table. +func NewTableData(gvr client.GVR) *TableData { + return &TableData{ + gvr: gvr, + rowEvents: NewRowEvents(10), + } +} + +func NewTableDataFull(gvr client.GVR, ns string, h Header, re *RowEvents) *TableData { + t := NewTableDataWithRows(gvr, h, re) + t.namespace = ns + + return t +} + +func NewTableDataWithRows(gvr client.GVR, h Header, re *RowEvents) *TableData { + t := NewTableData(gvr) + t.header, t.rowEvents = h, re + + return t +} + +func NewTableDataFromTable(td *TableData) *TableData { + t := NewTableData(td.gvr) + t.header = td.header + t.rowEvents = td.rowEvents + t.namespace = td.namespace + + return t +} + +func (t *TableData) AddRow(re RowEvent) { + t.rowEvents.Add(re) +} + +func (t *TableData) SetRow(idx int, re RowEvent) { + t.rowEvents.Set(idx, re) +} + +func (t *TableData) FindRow(id string) (RowEvent, bool) { + return t.rowEvents.Get(id) +} + +func (t *TableData) RowAt(idx int) (RowEvent, bool) { + return t.rowEvents.At(idx) +} + +func (t *TableData) RowsRange(f ReRangeFn) { + t.rowEvents.Range(f) +} + +func (t *TableData) Sort(sc SortColumn) { + col, idx := t.HeadCol(sc.Name, false) + if idx < 0 { + return + } + t.rowEvents.Sort( + t.GetNamespace(), + idx, + col.Time, + col.MX, + col.Capacity, + sc.ASC, + ) +} + +func (t *TableData) Header() Header { + return t.header +} + +// HeaderCount returns the number of header cols. +func (t *TableData) HeaderCount() int { + t.mx.RLock() + defer t.mx.RUnlock() + + return len(t.header) +} + +func (t *TableData) HeadCol(n string, w bool) (HeaderColumn, int) { + idx, ok := t.header.IndexOf(n, w) + if !ok { + return HeaderColumn{}, -1 + } + + return t.header[idx], idx +} + +func (t *TableData) Filter(f FilterOpts) *TableData { + td := NewTableDataFromTable(t) + + if f.Toast { + td.rowEvents = t.filterToast() + } + if f.Filter == "" || internal.IsLabelSelector(f.Filter) { + return td + } + if f, ok := internal.IsFuzzySelector(f.Filter); ok { + td.rowEvents = t.fuzzyFilter(f) + return td + } + rr, err := t.rxFilter(f.Filter, internal.IsInverseSelector(f.Filter)) + if err == nil { + td.rowEvents = rr + } else { + log.Error().Err(err).Msg("rx filter failed") + } + + return td +} + +func (t *TableData) rxFilter(q string, inverse bool) (*RowEvents, error) { + if inverse { + q = q[1:] + } + rx, err := regexp.Compile(`(?i)(` + q + `)`) + if err != nil { + return nil, fmt.Errorf("invalid rx filter %q: %w", q, err) + } + + ageIndex, ok := t.header.IndexOf("AGE", true) + + rr := NewRowEvents(t.RowCount() / 2) + t.rowEvents.Range(func(_ int, re RowEvent) bool { + ff := re.Row.Fields + if ok && ageIndex+1 <= len(ff) { + ff = append(ff[0:ageIndex], ff[ageIndex+1:]...) + } + fields := strings.Join(ff, spacer) + if (inverse && !rx.MatchString(fields)) || + ((!inverse) && rx.MatchString(fields)) { + rr.Add(re) + } + return true + }) + + return rr, nil +} + +func (t *TableData) fuzzyFilter(q string) *RowEvents { + q = strings.TrimSpace(q) + ss := make([]string, 0, t.RowCount()/2) + t.rowEvents.Range(func(_ int, re RowEvent) bool { + ss = append(ss, re.Row.ID) + return true + }) + + mm := fuzzy.Find(q, ss) + rr := NewRowEvents(t.RowCount() / 2) + for _, m := range mm { + re, ok := t.rowEvents.At(m.Index) + if !ok { + log.Error().Msgf("unable to find event for index in fuzzfilter: %d", m.Index) + continue + } + rr.Add(re) + } + + return rr +} + +func (t *TableData) filterToast() *RowEvents { + idx, ok := t.header.IndexOf("VALID", true) + if !ok { + return nil + } + + rr := NewRowEvents(10) + t.rowEvents.Range(func(_ int, re RowEvent) bool { + if re.Row.Fields[idx] != "" { + rr.Add(re) + } + return true + }) + + return rr +} + +func (t *TableData) GetNamespace() string { + t.mx.RLock() + defer t.mx.RUnlock() + + return t.namespace +} + +func (t *TableData) Reset(ns string) { + t.mx.Lock() + { + t.namespace = ns + } + t.mx.Unlock() + + t.Clear() +} + +func (t *TableData) Reconcile(ctx context.Context, r Renderer, oo []runtime.Object) error { + var rows Rows + if len(oo) > 0 { + if r.IsGeneric() { + table, ok := oo[0].(*metav1.Table) + if !ok { + return fmt.Errorf("expecting a meta table but got %T", oo[0]) + } + rows = make(Rows, len(table.Rows)) + if err := GenericHydrate(t.namespace, table, rows, r); err != nil { + return err + } + } else { + rows = make(Rows, len(oo)) + if err := Hydrate(t.namespace, oo, rows, r); err != nil { + return err + } + } + } + + t.Update(rows) + t.SetHeader(t.namespace, r.Header(t.namespace)) + if t.HeaderCount() == 0 { + return fmt.Errorf("fail to list resource %s", t.gvr) + } + + return nil +} + +// Empty checks if there are no entries. +func (t *TableData) Empty() bool { + t.mx.RLock() + defer t.mx.RUnlock() + + return t.rowEvents.Empty() +} + +func (t *TableData) SetRowEvents(re *RowEvents) { + t.rowEvents = re +} + +func (t *TableData) GetRowEvents() *RowEvents { + return t.rowEvents +} + +// RowCount returns the number of rows. +func (t *TableData) RowCount() int { + t.mx.RLock() + defer t.mx.RUnlock() + + return t.rowEvents.Len() +} + +// IndexOfHeader return the index of the header. +func (t *TableData) IndexOfHeader(h string) (int, bool) { + return t.header.IndexOf(h, false) +} + +// Labelize prints out specific label columns. +func (t *TableData) Labelize(labels []string) *TableData { + idx, ok := t.header.IndexOf("LABELS", true) + if !ok { + return t + } + cols := []int{0, 1} + if client.IsNamespaced(t.namespace) { + cols = cols[1:] + } + data := TableData{ + namespace: t.namespace, + header: t.header.Labelize(cols, idx, t.rowEvents), + } + data.rowEvents = t.rowEvents.Labelize(cols, idx, labels) + + return &data +} + +// Customize returns a new model with customized column layout. +func (t *TableData) Customize(vs *config.ViewSetting, sc SortColumn, manual, wide bool) (*TableData, SortColumn) { + if vs.IsBlank() { + if sc.Name != "" { + return t, sc + } + psc, err := t.sortCol(vs) + if err == nil { + return t, psc + } + return t, sc + } + + cols := vs.Columns + cdata := TableData{ + gvr: t.gvr, + namespace: t.namespace, + header: t.header.Customize(cols, wide), + } + ids := t.header.MapIndices(cols, wide) + cdata.rowEvents = t.rowEvents.Customize(ids) + if manual || vs == nil { + return &cdata, sc + } + psc, err := cdata.sortCol(vs) + if err != nil { + return &cdata, sc + } + + return &cdata, psc +} + +func (t *TableData) sortCol(vs *config.ViewSetting) (SortColumn, error) { + var psc SortColumn + + if t.HeaderCount() == 0 { + return psc, errors.New("no header found") + } + name, order, _ := vs.SortCol() + if _, ok := t.header.IndexOf(name, false); ok { + psc.Name, psc.ASC = name, order + return psc, nil + } + if client.IsAllNamespaces(t.GetNamespace()) { + if _, ok := t.header.IndexOf("NAMESPACE", false); ok { + psc.Name = "NAMESPACE" + } else if _, ok := t.header.IndexOf("NAME", false); ok { + psc.Name = "NAME" + } + } else { + if _, ok := t.header.IndexOf("NAME", false); ok { + psc.Name = "NAME" + } else { + psc.Name = t.header[0].Name + } + } + psc.ASC = true + + return psc, nil +} + +// Clear clears out the entire table. +func (t *TableData) Clear() { + t.mx.Lock() + defer t.mx.Unlock() + + t.header = t.header.Clear() + t.rowEvents.Clear() +} + +// Clone returns a copy of the table. +func (t *TableData) Clone() *TableData { + t.mx.RLock() + defer t.mx.RUnlock() + + return &TableData{ + header: t.header.Clone(), + rowEvents: t.rowEvents.Clone(), + namespace: t.namespace, + gvr: t.gvr, + } +} + +func (t *TableData) ColumnNames(w bool) []string { + t.mx.RLock() + defer t.mx.RUnlock() + + return t.header.ColumnNames(w) +} + +// GetHeader returns table header. +func (t *TableData) GetHeader() Header { + t.mx.RLock() + defer t.mx.RUnlock() + + return t.header +} + +// SetHeader sets table header. +func (t *TableData) SetHeader(ns string, h Header) { + t.mx.Lock() + defer t.mx.Unlock() + + t.namespace, t.header = ns, h +} + +// Update computes row deltas and update the table data. +func (t *TableData) Update(rows Rows) { + empty := t.Empty() + kk := make(map[string]struct{}, len(rows)) + var blankDelta DeltaRow + t.mx.Lock() + { + for _, row := range rows { + kk[row.ID] = struct{}{} + if empty { + t.rowEvents.Add(NewRowEvent(EventAdd, row)) + continue + } + if index, ok := t.rowEvents.FindIndex(row.ID); ok { + ev, ok := t.rowEvents.At(index) + if !ok { + continue + } + delta := NewDeltaRow(ev.Row, row, t.header) + if delta.IsBlank() { + ev.Kind, ev.Deltas, ev.Row = EventUnchanged, blankDelta, row + t.rowEvents.Set(index, ev) + } else { + t.rowEvents.Set(index, NewRowEventWithDeltas(row, delta)) + } + continue + } + t.rowEvents.Add(NewRowEvent(EventAdd, row)) + } + } + t.mx.Unlock() + + if !empty { + t.Delete(kk) + } +} + +// Delete removes items in cache that are no longer valid. +func (t *TableData) Delete(newKeys map[string]struct{}) { + t.mx.Lock() + { + victims := make([]string, 0, 10) + t.rowEvents.Range(func(_ int, e RowEvent) bool { + if _, ok := newKeys[e.Row.ID]; !ok { + victims = append(victims, e.Row.ID) + } else { + delete(newKeys, e.Row.ID) + } + return true + }) + for _, id := range victims { + if err := t.rowEvents.Delete(id); err != nil { + log.Error().Err(err).Msgf("table delete failed: %q", id) + } + } + } + t.mx.Unlock() +} + +// Diff checks if two tables are equal. +func (t *TableData) Diff(t2 *TableData) bool { + if t2 == nil || t.namespace != t2.namespace || t.header.Diff(t2.header) { + return true + } + idx, ok := t.header.IndexOf("AGE", true) + if !ok { + idx = -1 + } + return t.rowEvents.Diff(t2.rowEvents, idx) +} diff --git a/internal/model1/table_data_test.go b/internal/model1/table_data_test.go new file mode 100644 index 0000000000..fc338a5649 --- /dev/null +++ b/internal/model1/table_data_test.go @@ -0,0 +1,404 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1 + +import ( + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/config" + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func init() { + zerolog.SetGlobalLevel(zerolog.FatalLevel) +} + +func TestTableDataCustomize(t *testing.T) { + uu := map[string]struct { + t1, e *TableData + vs config.ViewSetting + sc SortColumn + wide, manual bool + }{ + "same": { + t1: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + vs: config.ViewSetting{Columns: []string{"A", "B", "C"}}, + e: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + }, + "wide-col": { + t1: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B", Wide: true}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + vs: config.ViewSetting{Columns: []string{"A", "B", "C"}}, + e: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B", Wide: false}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + }, + "wide": { + t1: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B", Wide: true}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + wide: true, + vs: config.ViewSetting{Columns: []string{"A", "C"}}, + e: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "C"}, + HeaderColumn{Name: "B", Wide: true}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "3", "2"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "3", "2"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "3", "2"}}}, + ), + ), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + td, _ := u.t1.Customize(&u.vs, u.sc, u.manual, u.wide) + assert.Equal(t, u.e, td) + }) + } +} + +func TestTableDataDiff(t *testing.T) { + uu := map[string]struct { + t1, t2 *TableData + e bool + }{ + "empty": { + t1: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + e: true, + }, + "same": { + t1: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + t2: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + }, + "ns-diff": { + t1: NewTableDataFull( + client.NewGVR("test"), + "ns1", + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + t2: NewTableDataFull( + client.NewGVR("test"), + "ns-2", + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + e: true, + }, + "header-diff": { + t1: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "D"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + t2: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + e: true, + }, + "row-diff": { + t1: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + ), + t2: NewTableDataWithRows( + client.NewGVR("test"), + Header{ + HeaderColumn{Name: "A"}, + HeaderColumn{Name: "B"}, + HeaderColumn{Name: "C"}, + }, + NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"100", "2", "3"}}}, + ), + ), + e: true, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.t1.Diff(u.t2)) + }) + } +} + +func TestTableDataUpdate(t *testing.T) { + uu := map[string]struct { + re, e *RowEvents + rr Rows + }{ + "no-change": { + re: NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + rr: Rows{ + Row{ID: "A", Fields: Fields{"1", "2", "3"}}, + Row{ID: "B", Fields: Fields{"0", "2", "3"}}, + Row{ID: "C", Fields: Fields{"10", "2", "3"}}, + }, + e: NewRowEventsWithEvts( + RowEvent{Kind: EventUnchanged, Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Kind: EventUnchanged, Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Kind: EventUnchanged, Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + }, + "add": { + re: NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + rr: Rows{ + Row{ID: "A", Fields: Fields{"1", "2", "3"}}, + Row{ID: "B", Fields: Fields{"0", "2", "3"}}, + Row{ID: "C", Fields: Fields{"10", "2", "3"}}, + Row{ID: "D", Fields: Fields{"10", "2", "3"}}, + }, + e: NewRowEventsWithEvts( + RowEvent{Kind: EventUnchanged, Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Kind: EventUnchanged, Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Kind: EventUnchanged, Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + RowEvent{Kind: EventAdd, Row: Row{ID: "D", Fields: Fields{"10", "2", "3"}}}, + ), + }, + "delete": { + re: NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + rr: Rows{ + Row{ID: "A", Fields: Fields{"1", "2", "3"}}, + Row{ID: "C", Fields: Fields{"10", "2", "3"}}, + }, + e: NewRowEventsWithEvts( + RowEvent{Kind: EventUnchanged, Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Kind: EventUnchanged, Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + }, + "update": { + re: NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + rr: Rows{ + Row{ID: "A", Fields: Fields{"10", "2", "3"}}, + Row{ID: "B", Fields: Fields{"0", "2", "3"}}, + Row{ID: "C", Fields: Fields{"10", "2", "3"}}, + }, + e: NewRowEventsWithEvts( + RowEvent{ + Kind: EventUpdate, + Row: Row{ID: "A", Fields: Fields{"10", "2", "3"}}, + Deltas: DeltaRow{"1", "", ""}, + }, + RowEvent{Kind: EventUnchanged, Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Kind: EventUnchanged, Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + }, + } + + var table TableData + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + table.SetRowEvents(u.re) + table.Update(u.rr) + assert.Equal(t, u.e, table.GetRowEvents()) + }) + } +} + +func TestTableDataDelete(t *testing.T) { + uu := map[string]struct { + re, e *RowEvents + kk map[string]struct{} + }{ + "ordered": { + re: NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + kk: map[string]struct{}{"A": {}, "C": {}}, + e: NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + }, + "unordered": { + re: NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "B", Fields: Fields{"0", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + RowEvent{Row: Row{ID: "D", Fields: Fields{"10", "2", "3"}}}, + ), + kk: map[string]struct{}{"C": {}, "A": {}}, + e: NewRowEventsWithEvts( + RowEvent{Row: Row{ID: "A", Fields: Fields{"1", "2", "3"}}}, + RowEvent{Row: Row{ID: "C", Fields: Fields{"10", "2", "3"}}}, + ), + }, + } + + var table TableData + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + table.SetRowEvents(u.re) + table.Delete(u.kk) + assert.Equal(t, u.e, table.GetRowEvents()) + }) + } +} diff --git a/internal/model1/test_helper_test.go b/internal/model1/test_helper_test.go new file mode 100644 index 0000000000..42350b333a --- /dev/null +++ b/internal/model1/test_helper_test.go @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1_test + +import ( + "fmt" + "time" +) + +func testTime() time.Time { + t, err := time.Parse(time.RFC3339, "2018-12-14T10:36:43.326972-07:00") + if err != nil { + fmt.Println("TestTime Failed", err) + } + return t +} diff --git a/internal/model1/types.go b/internal/model1/types.go new file mode 100644 index 0000000000..2fc32ad278 --- /dev/null +++ b/internal/model1/types.go @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package model1 + +import ( + "github.com/derailed/tcell/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + NAValue = "na" + + // EventUnchanged notifies listener resource has not changed. + EventUnchanged ResEvent = 1 << iota + + // EventAdd notifies listener of a resource was added. + EventAdd + + // EventUpdate notifies listener of a resource updated. + EventUpdate + + // EventDelete notifies listener of a resource was deleted. + EventDelete + + // EventClear the stack was reset. + EventClear +) + +// DecoratorFunc decorates a string. +type DecoratorFunc func(string) string + +// ColorerFunc represents a resource row colorer. +type ColorerFunc func(ns string, h Header, re *RowEvent) tcell.Color + +// Renderer represents a resource renderer. +type Renderer interface { + // IsGeneric identifies a generic handler. + IsGeneric() bool + + // Render converts raw resources to tabular data. + Render(o interface{}, ns string, row *Row) error + + // Header returns the resource header. + Header(ns string) Header + + // ColorerFunc returns a row colorer function. + ColorerFunc() ColorerFunc +} + +// Generic represents a generic resource. +type Generic interface { + // SetTable sets up the resource tabular definition. + SetTable(ns string, table *metav1.Table) + + // Header returns a resource header. + Header(ns string) Header + + // Render renders the resource. + Render(o interface{}, ns string, row *Row) error +} diff --git a/internal/perf/benchmark.go b/internal/perf/benchmark.go index 8cd529372e..5440e53a90 100644 --- a/internal/perf/benchmark.go +++ b/internal/perf/benchmark.go @@ -1,17 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package perf import ( "bytes" "context" "fmt" - "github.com/derailed/k9s/internal/dao" "io" "net/http" "os" "path/filepath" + "strings" "sync" "time" + "github.com/derailed/k9s/internal/config/data" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" "github.com/rakyll/hey/requester" @@ -25,11 +30,6 @@ const ( k9sUA = "k9s/" ) -var ( - // K9sBenchDir directory to store K9s Benchmark files. - K9sBenchDir = filepath.Join(os.TempDir(), fmt.Sprintf("k9s-bench-%s", config.MustK9sUser())) -) - // Benchmark puts a workload under load. type Benchmark struct { canceled bool @@ -107,39 +107,43 @@ func (b *Benchmark) Canceled() bool { } // Run starts a benchmark. -func (b *Benchmark) Run(cluster string, done func()) { - log.Debug().Msgf("Running benchmark on cluster %s", cluster) +func (b *Benchmark) Run(cluster, context string, done func()) { + log.Debug().Msgf("Running benchmark on context %s", cluster) buff := new(bytes.Buffer) b.worker.Writer = buff // this call will block until the benchmark is complete or times out. b.worker.Run() b.worker.Stop() if buff.Len() > 0 { - if err := b.save(cluster, buff); err != nil { + if err := b.save(cluster, context, buff); err != nil { log.Error().Err(err).Msg("Saving Benchmark") } } done() } -func (b *Benchmark) save(cluster string, r io.Reader) error { - dir := filepath.Join(K9sBenchDir, cluster) - if err := os.MkdirAll(dir, 0744); err != nil { +func (b *Benchmark) save(cluster, context string, r io.Reader) error { + ns, n := client.Namespaced(b.config.Name) + n = strings.Replace(n, "|", "_", -1) + n = strings.Replace(n, ":", "_", -1) + dir, err := config.EnsureBenchmarksDir(cluster, context) + if err != nil { + return err + } + bf := filepath.Join(dir, fmt.Sprintf(benchFmat, ns, n, time.Now().UnixNano())) + if err := data.EnsureDirPath(bf, data.DefaultDirMod); err != nil { return err } - ns, n := client.Namespaced(b.config.Name) - file := filepath.Join(dir, fmt.Sprintf(benchFmat, ns, dao.BenchRx.ReplaceAllString(n, "_"), time.Now().UnixNano())) - f, err := os.Create(file) + f, err := os.Create(bf) if err != nil { return err } defer func() { if e := f.Close(); e != nil { - log.Fatal().Err(e).Msg("Bench save") + log.Error().Err(e).Msgf("Benchmark file close failed: %q", bf) } }() - if _, err = io.Copy(f, r); err != nil { return err } diff --git a/internal/port/ann.go b/internal/port/ann.go index 8d7b12b4d6..17ebb589b3 100644 --- a/internal/port/ann.go +++ b/internal/port/ann.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package port import ( diff --git a/internal/port/ann_test.go b/internal/port/ann_test.go index 2351885791..336a4f37dc 100644 --- a/internal/port/ann_test.go +++ b/internal/port/ann_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package port_test import ( diff --git a/internal/port/co_portspec.go b/internal/port/co_portspec.go index 13c8ef8167..bddec9713e 100644 --- a/internal/port/co_portspec.go +++ b/internal/port/co_portspec.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package port import ( diff --git a/internal/port/co_portspec_test.go b/internal/port/co_portspec_test.go index 3886477a14..39f57040a3 100644 --- a/internal/port/co_portspec_test.go +++ b/internal/port/co_portspec_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package port_test import ( diff --git a/internal/port/pf.go b/internal/port/pf.go index fb4da5709f..76f8b6419a 100644 --- a/internal/port/pf.go +++ b/internal/port/pf.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package port import ( @@ -10,10 +13,10 @@ import ( ) const ( - // K9sAutoPortForwardKey represents an auto portforwards annotation. + // K9sAutoPortForwardsKey represents an auto portforwards annotation. K9sAutoPortForwardsKey = "k9scli.io/auto-port-forwards" - // K9sPortForwardKey represents a portforwards annotation. + // K9sPortForwardsKey represents a portforwards annotation. K9sPortForwardsKey = "k9scli.io/port-forwards" ) diff --git a/internal/port/pf_test.go b/internal/port/pf_test.go index 62b6bf99da..66a11d15a4 100644 --- a/internal/port/pf_test.go +++ b/internal/port/pf_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package port_test import ( diff --git a/internal/port/pfs.go b/internal/port/pfs.go index 10cc6bd0f7..85b0a2e515 100644 --- a/internal/port/pfs.go +++ b/internal/port/pfs.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package port import ( diff --git a/internal/port/pfs_test.go b/internal/port/pfs_test.go index 7b60d86693..3d003a47ac 100644 --- a/internal/port/pfs_test.go +++ b/internal/port/pfs_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package port_test import ( diff --git a/internal/port/tunnel.go b/internal/port/tunnel.go index 8074372abb..758675adc4 100644 --- a/internal/port/tunnel.go +++ b/internal/port/tunnel.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package port import ( diff --git a/internal/port/tunnel_test.go b/internal/port/tunnel_test.go index d88dab582f..763d412017 100644 --- a/internal/port/tunnel_test.go +++ b/internal/port/tunnel_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package port_test import ( diff --git a/internal/render/alias.go b/internal/render/alias.go index 2cb658166d..592296f36c 100644 --- a/internal/render/alias.go +++ b/internal/render/alias.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -5,6 +8,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -15,17 +19,17 @@ type Alias struct { } // Header returns a header row. -func (Alias) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "RESOURCE"}, - HeaderColumn{Name: "COMMAND"}, - HeaderColumn{Name: "APIGROUP"}, +func (Alias) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "RESOURCE"}, + model1.HeaderColumn{Name: "COMMAND"}, + model1.HeaderColumn{Name: "API-GROUP"}, } } // Render renders a K8s resource to screen. // BOZO!! Pass in a row with pre-alloc fields?? -func (Alias) Render(o interface{}, ns string, r *Row) error { +func (Alias) Render(o interface{}, ns string, r *model1.Row) error { a, ok := o.(AliasRes) if !ok { return fmt.Errorf("expected AliasRes, but got %T", o) diff --git a/internal/render/alias_test.go b/internal/render/alias_test.go index 18c0e5ae95..af85a3f69c 100644 --- a/internal/render/alias_test.go +++ b/internal/render/alias_test.go @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/derailed/tcell/v2" "github.com/stretchr/testify/assert" @@ -11,30 +15,30 @@ import ( func TestAliasColorer(t *testing.T) { var a render.Alias - h := render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, + h := model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "C"}, } - r := render.Row{ID: "g/v/r", Fields: render.Fields{"r", "blee", "g"}} + r := model1.Row{ID: "g/v/r", Fields: model1.Fields{"r", "blee", "g"}} uu := map[string]struct { ns string - re render.RowEvent + re model1.RowEvent e tcell.Color }{ "addAll": { - ns: client.AllNamespaces, - re: render.RowEvent{Kind: render.EventAdd, Row: r}, + ns: client.NamespaceAll, + re: model1.RowEvent{Kind: model1.EventAdd, Row: r}, e: tcell.ColorBlue, }, "deleteAll": { - ns: client.AllNamespaces, - re: render.RowEvent{Kind: render.EventDelete, Row: r}, + ns: client.NamespaceAll, + re: model1.RowEvent{Kind: model1.EventDelete, Row: r}, e: tcell.ColorGray, }, "updateAll": { - ns: client.AllNamespaces, - re: render.RowEvent{Kind: render.EventUpdate, Row: r}, + ns: client.NamespaceAll, + re: model1.RowEvent{Kind: model1.EventUpdate, Row: r}, e: tcell.ColorDefault, }, } @@ -42,21 +46,21 @@ func TestAliasColorer(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, a.ColorerFunc()(u.ns, h, u.re)) + assert.Equal(t, u.e, a.ColorerFunc()(u.ns, h, &u.re)) }) } } func TestAliasHeader(t *testing.T) { - h := render.Header{ - render.HeaderColumn{Name: "RESOURCE"}, - render.HeaderColumn{Name: "COMMAND"}, - render.HeaderColumn{Name: "APIGROUP"}, + h := model1.Header{ + model1.HeaderColumn{Name: "RESOURCE"}, + model1.HeaderColumn{Name: "COMMAND"}, + model1.HeaderColumn{Name: "API-GROUP"}, } var a render.Alias assert.Equal(t, h, a.Header("fred")) - assert.Equal(t, h, a.Header(client.AllNamespaces)) + assert.Equal(t, h, a.Header(client.NamespaceAll)) } func TestAliasRender(t *testing.T) { @@ -67,9 +71,9 @@ func TestAliasRender(t *testing.T) { Aliases: []string{"a", "b", "c"}, } - var r render.Row + var r model1.Row assert.Nil(t, a.Render(o, "fred/v1/blee", &r)) - assert.Equal(t, render.Row{ID: "fred/v1/blee", Fields: render.Fields{"blee", "a,b,c", "fred"}}, r) + assert.Equal(t, model1.Row{ID: "fred/v1/blee", Fields: model1.Fields{"blee", "a,b,c", "fred"}}, r) } func BenchmarkAlias(b *testing.B) { @@ -82,7 +86,7 @@ func BenchmarkAlias(b *testing.B) { b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { - var r render.Row + var r model1.Row _ = a.Render(o, "aliases", &r) } } diff --git a/internal/render/base.go b/internal/render/base.go index 3a668993be..003fe6a860 100644 --- a/internal/render/base.go +++ b/internal/render/base.go @@ -1,5 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render +import ( + "github.com/derailed/k9s/internal/model1" +) + // DecoratorFunc decorates a string. type DecoratorFunc func(string) string @@ -16,11 +23,11 @@ func (Base) IsGeneric() bool { } // ColorerFunc colors a resource row. -func (Base) ColorerFunc() ColorerFunc { - return DefaultColorer +func (Base) ColorerFunc() model1.ColorerFunc { + return model1.DefaultColorer } // Happy returns true if resource is happy, false otherwise. -func (Base) Happy(_ string, _ Row) bool { +func (Base) Happy(string, *model1.Row) bool { return true } diff --git a/internal/render/benchmark.go b/internal/render/benchmark.go index 2b60cff68c..ded85e3170 100644 --- a/internal/render/benchmark.go +++ b/internal/render/benchmark.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -9,8 +12,10 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" "github.com/derailed/tview" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -29,56 +34,57 @@ type Benchmark struct { } // ColorerFunc colors a resource row. -func (b Benchmark) ColorerFunc() ColorerFunc { - return func(ns string, h Header, re RowEvent) tcell.Color { - if !Happy(ns, h, re.Row) { - return ErrColor +func (b Benchmark) ColorerFunc() model1.ColorerFunc { + return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { + if !model1.IsValid(ns, h, re.Row) { + return model1.ErrColor } + return tcell.ColorPaleGreen } } // Header returns a header row. -func (Benchmark) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "STATUS"}, - HeaderColumn{Name: "TIME"}, - HeaderColumn{Name: "REQ/S", Align: tview.AlignRight}, - HeaderColumn{Name: "2XX", Align: tview.AlignRight}, - HeaderColumn{Name: "4XX/5XX", Align: tview.AlignRight}, - HeaderColumn{Name: "REPORT"}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (Benchmark) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "TIME"}, + model1.HeaderColumn{Name: "REQ/S", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "2XX", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "4XX/5XX", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "REPORT"}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (b Benchmark) Render(o interface{}, ns string, r *Row) error { +func (b Benchmark) Render(o interface{}, ns string, r *model1.Row) error { bench, ok := o.(BenchInfo) if !ok { - return fmt.Errorf("No benchmarks available %T", o) + return fmt.Errorf("no benchmarks available %T", o) } data, err := b.readFile(bench.Path) if err != nil { - return fmt.Errorf("Unable to load bench file %s", bench.Path) + return fmt.Errorf("unable to load bench file %s", bench.Path) } r.ID = bench.Path - r.Fields = make(Fields, len(b.Header(ns))) + r.Fields = make(model1.Fields, len(b.Header(ns))) if err := b.initRow(r.Fields, bench.File); err != nil { return err } b.augmentRow(r.Fields, data) - r.Fields[8] = asStatus(b.diagnose(ns, r.Fields)) + r.Fields[8] = AsStatus(b.diagnose(ns, r.Fields)) return nil } // Happy returns true if resource is happy, false otherwise. -func (Benchmark) diagnose(ns string, ff Fields) error { +func (Benchmark) diagnose(ns string, ff model1.Fields) error { statusCol := 3 if !client.IsAllNamespaces(ns) { statusCol-- @@ -105,20 +111,20 @@ func (Benchmark) readFile(file string) (string, error) { return string(data), nil } -func (b Benchmark) initRow(row Fields, f os.FileInfo) error { +func (b Benchmark) initRow(row model1.Fields, f os.FileInfo) error { tokens := strings.Split(f.Name(), "_") if len(tokens) < 2 { - return fmt.Errorf("Invalid file name %s", f.Name()) + return fmt.Errorf("invalid file name %s", f.Name()) } row[0] = tokens[0] row[1] = tokens[1] row[7] = f.Name() - row[9] = timeToAge(f.ModTime()) + row[9] = ToAge(metav1.Time{Time: f.ModTime()}) return nil } -func (b Benchmark) augmentRow(fields Fields, data string) { +func (b Benchmark) augmentRow(fields model1.Fields, data string) { if len(data) == 0 { return } diff --git a/internal/render/benchmark_int_test.go b/internal/render/benchmark_int_test.go index 4fe0963948..a7d4a387c6 100644 --- a/internal/render/benchmark_int_test.go +++ b/internal/render/benchmark_int_test.go @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( "os" "testing" + "github.com/derailed/k9s/internal/model1" "github.com/rs/zerolog" "github.com/stretchr/testify/assert" ) @@ -15,23 +19,23 @@ func init() { func TestAugmentRow(t *testing.T) { uu := map[string]struct { file string - e Fields + e model1.Fields }{ "cool": { "testdata/b1.txt", - Fields{"pass", "3.3544", "29.8116", "100", "0"}, + model1.Fields{"pass", "3.3544", "29.8116", "100", "0"}, }, "2XX": { "testdata/b4.txt", - Fields{"pass", "3.3544", "29.8116", "160", "0"}, + model1.Fields{"pass", "3.3544", "29.8116", "160", "0"}, }, "4XX/5XX": { "testdata/b2.txt", - Fields{"pass", "3.3544", "29.8116", "100", "12"}, + model1.Fields{"pass", "3.3544", "29.8116", "100", "12"}, }, "toast": { "testdata/b3.txt", - Fields{"fail", "2.3688", "35.4606", "0", "0"}, + model1.Fields{"fail", "2.3688", "35.4606", "0", "0"}, }, } @@ -41,7 +45,7 @@ func TestAugmentRow(t *testing.T) { data, err := os.ReadFile(u.file) assert.Nil(t, err) - fields := make(Fields, 8) + fields := make(model1.Fields, 8) b := Benchmark{} b.augmentRow(fields, string(data)) assert.Equal(t, u.e, fields[2:7]) diff --git a/internal/render/cm.go b/internal/render/cm.go new file mode 100644 index 0000000000..f6158efbeb --- /dev/null +++ b/internal/render/cm.go @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package render + +import ( + "fmt" + "strconv" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// ConfigMap renders a K8s ConfigMap to screen. +type ConfigMap struct { + Base +} + +// Header returns a header rbw. +func (ConfigMap) Header(string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "DATA"}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, + } +} + +// Render renders a K8s resource to screen. +func (n ConfigMap) Render(o interface{}, _ string, r *model1.Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("expected ConfigMap, but got %T", o) + } + var cm v1.ConfigMap + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &cm) + if err != nil { + return err + } + + r.ID = client.FQN(cm.Namespace, cm.Name) + r.Fields = model1.Fields{ + cm.Namespace, + cm.Name, + strconv.Itoa(len(cm.Data)), + "", + ToAge(cm.GetCreationTimestamp()), + } + + return nil +} diff --git a/internal/render/color_test.go b/internal/render/color_test.go deleted file mode 100644 index 77bb4c51fa..0000000000 --- a/internal/render/color_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package render_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/render" - "github.com/derailed/tcell/v2" - "github.com/stretchr/testify/assert" -) - -func TestDefaultColorer(t *testing.T) { - uu := map[string]struct { - re render.RowEvent - e tcell.Color - }{ - "add": { - render.RowEvent{ - Kind: render.EventAdd, - }, - render.AddColor, - }, - "update": { - render.RowEvent{ - Kind: render.EventUpdate, - }, - render.ModColor, - }, - "delete": { - render.RowEvent{ - Kind: render.EventDelete, - }, - render.KillColor, - }, - "no-change": { - render.RowEvent{ - Kind: render.EventUnchanged, - }, - render.StdColor, - }, - "invalid": { - render.RowEvent{ - Kind: render.EventUnchanged, - Row: render.Row{ - Fields: render.Fields{"", "", "blah"}, - }, - }, - render.ErrColor, - }, - } - - h := render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "VALID"}, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, render.DefaultColorer("", h, u.re)) - }) - } -} diff --git a/internal/render/container.go b/internal/render/container.go index e879265e3e..35f700f267 100644 --- a/internal/render/container.go +++ b/internal/render/container.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -7,6 +10,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" "github.com/derailed/tview" v1 "k8s.io/api/core/v1" @@ -40,63 +44,61 @@ type Container struct { } // ColorerFunc colors a resource row. -func (c Container) ColorerFunc() ColorerFunc { - return func(ns string, h Header, re RowEvent) tcell.Color { - if !Happy(ns, h, re.Row) { - return ErrColor - } +func (c Container) ColorerFunc() model1.ColorerFunc { + return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { + c := model1.DefaultColorer(ns, h, re) - stateCol := h.IndexOf("STATE", true) - if stateCol == -1 { - return DefaultColorer(ns, h, re) + idx, ok := h.IndexOf("STATE", true) + if !ok { + return c } - switch strings.TrimSpace(re.Row.Fields[stateCol]) { + switch strings.TrimSpace(re.Row.Fields[idx]) { case Pending: - return PendingColor + return model1.PendingColor case ContainerCreating, PodInitializing: - return AddColor + return model1.AddColor case Terminating, Initialized: - return HighlightColor + return model1.HighlightColor case Completed: - return CompletedColor + return model1.CompletedColor case Running: - return DefaultColorer(ns, h, re) + return c default: - return ErrColor + return model1.ErrColor } } } // Header returns a header row. -func (Container) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "PF"}, - HeaderColumn{Name: "IMAGE"}, - HeaderColumn{Name: "READY"}, - HeaderColumn{Name: "STATE"}, - HeaderColumn{Name: "INIT"}, - HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight}, - HeaderColumn{Name: "PROBES(L:R)"}, - HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "CPU/R:L", Align: tview.AlignRight}, - HeaderColumn{Name: "MEM/R:L", Align: tview.AlignRight}, - HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "%CPU/L", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "%MEM/R", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "%MEM/L", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "PORTS"}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (Container) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "PF"}, + model1.HeaderColumn{Name: "IMAGE"}, + model1.HeaderColumn{Name: "READY"}, + model1.HeaderColumn{Name: "STATE"}, + model1.HeaderColumn{Name: "INIT"}, + model1.HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "PROBES(L:R)"}, + model1.HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "CPU/R:L", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "MEM/R:L", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "%CPU/L", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "%MEM/R", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "%MEM/L", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "PORTS"}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (c Container) Render(o interface{}, name string, r *Row) error { +func (c Container) Render(o interface{}, name string, r *model1.Row) error { co, ok := o.(ContainerRes) if !ok { - return fmt.Errorf("Expected ContainerRes, but got %T", o) + return fmt.Errorf("expected ContainerRes, but got %T", o) } cur, res := gatherMetrics(co.Container, co.MX) @@ -106,7 +108,7 @@ func (c Container) Render(o interface{}, name string, r *Row) error { } r.ID = co.Container.Name - r.Fields = Fields{ + r.Fields = model1.Fields{ co.Container.Name, "●", co.Container.Image, @@ -124,8 +126,8 @@ func (c Container) Render(o interface{}, name string, r *Row) error { client.ToPercentageStr(cur.mem, res.mem), client.ToPercentageStr(cur.mem, res.lmem), ToContainerPorts(co.Container.Ports), - asStatus(c.diagnose(state, ready)), - toAge(co.Age), + AsStatus(c.diagnose(state, ready)), + ToAge(co.Age), } return nil @@ -146,17 +148,30 @@ func (Container) diagnose(state, ready string) error { // ---------------------------------------------------------------------------- // Helpers... +func containerRequests(co *v1.Container) v1.ResourceList { + req := co.Resources.Requests + if len(req) != 0 { + return req + } + lim := co.Resources.Limits + if len(lim) != 0 { + return lim + } + + return nil +} + func gatherMetrics(co *v1.Container, mx *mv1beta1.ContainerMetrics) (c, r metric) { rList, lList := containerRequests(co), co.Resources.Limits if rList.Cpu() != nil { r.cpu = rList.Cpu().MilliValue() } - if lList.Cpu() != nil { - r.lcpu = lList.Cpu().MilliValue() - } if rList.Memory() != nil { r.mem = rList.Memory().Value() } + if lList.Cpu() != nil { + r.lcpu = lList.Cpu().MilliValue() + } if lList.Memory() != nil { r.lmem = lList.Memory().Value() } diff --git a/internal/render/container_test.go b/internal/render/container_test.go index f826ab595d..f790b3a541 100644 --- a/internal/render/container_test.go +++ b/internal/render/container_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( @@ -5,6 +8,7 @@ import ( "testing" "time" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" @@ -23,10 +27,10 @@ func TestContainer(t *testing.T) { IsInit: false, Age: makeAge(), } - var r render.Row + var r model1.Row assert.Nil(t, c.Render(cres, "blee", &r)) assert.Equal(t, "fred", r.ID) - assert.Equal(t, render.Fields{ + assert.Equal(t, model1.Fields{ "fred", "●", "img", @@ -60,7 +64,7 @@ func BenchmarkContainerRender(b *testing.B) { IsInit: false, Age: makeAge(), } - var r render.Row + var r model1.Row b.ReportAllocs() b.ResetTimer() diff --git a/internal/render/context.go b/internal/render/context.go index f4f1c54b06..06a622ad62 100644 --- a/internal/render/context.go +++ b/internal/render/context.go @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( "fmt" "strings" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" "github.com/rs/zerolog/log" "k8s.io/apimachinery/pkg/runtime" @@ -17,11 +21,11 @@ type Context struct { } // ColorerFunc colors a resource row. -func (Context) ColorerFunc() ColorerFunc { - return func(ns string, h Header, r RowEvent) tcell.Color { - c := DefaultColorer(ns, h, r) +func (Context) ColorerFunc() model1.ColorerFunc { + return func(ns string, h model1.Header, r *model1.RowEvent) tcell.Color { + c := model1.DefaultColorer(ns, h, r) if strings.Contains(strings.TrimSpace(r.Row.Fields[0]), "*") { - return HighlightColor + return model1.HighlightColor } return c @@ -29,17 +33,17 @@ func (Context) ColorerFunc() ColorerFunc { } // Header returns a header row. -func (Context) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "CLUSTER"}, - HeaderColumn{Name: "AUTHINFO"}, - HeaderColumn{Name: "NAMESPACE"}, +func (Context) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "CLUSTER"}, + model1.HeaderColumn{Name: "AUTHINFO"}, + model1.HeaderColumn{Name: "NAMESPACE"}, } } // Render renders a K8s resource to screen. -func (c Context) Render(o interface{}, _ string, r *Row) error { +func (c Context) Render(o interface{}, _ string, r *model1.Row) error { ctx, ok := o.(*NamedContext) if !ok { return fmt.Errorf("expected *NamedContext, but got %T", o) @@ -51,7 +55,7 @@ func (c Context) Render(o interface{}, _ string, r *Row) error { } r.ID = ctx.Name - r.Fields = Fields{ + r.Fields = model1.Fields{ name, ctx.Context.Cluster, ctx.Context.AuthInfo, diff --git a/internal/render/context_test.go b/internal/render/context_test.go index b1ebbd96d9..1cdc3911f5 100644 --- a/internal/render/context_test.go +++ b/internal/render/context_test.go @@ -1,8 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" "k8s.io/client-go/tools/clientcmd/api" @@ -17,7 +21,7 @@ func TestContextHeader(t *testing.T) { func TestContextRender(t *testing.T) { uu := map[string]struct { ctx *render.NamedContext - e render.Row + e model1.Row }{ "active": { ctx: &render.NamedContext{ @@ -30,9 +34,9 @@ func TestContextRender(t *testing.T) { }, Config: &config{}, }, - e: render.Row{ + e: model1.Row{ ID: "c1", - Fields: render.Fields{"c1", "c1", "u1", "ns1"}, + Fields: model1.Fields{"c1", "c1", "u1", "ns1"}, }, }, } @@ -41,7 +45,7 @@ func TestContextRender(t *testing.T) { for k := range uu { uc := uu[k] t.Run(k, func(t *testing.T) { - row := render.NewRow(4) + row := model1.NewRow(4) err := r.Render(uc.ctx, "", &row) assert.Nil(t, err) diff --git a/internal/render/cr.go b/internal/render/cr.go index c115d2e587..a148fc70b5 100644 --- a/internal/render/cr.go +++ b/internal/render/cr.go @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( "fmt" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -15,16 +19,16 @@ type ClusterRole struct { } // Header returns a header rbw. -func (ClusterRole) Header(string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (ClusterRole) Header(string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (ClusterRole) Render(o interface{}, ns string, r *Row) error { +func (ClusterRole) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { return fmt.Errorf("expecting clusterrole, but got %T", o) @@ -36,10 +40,10 @@ func (ClusterRole) Render(o interface{}, ns string, r *Row) error { } r.ID = client.FQN("-", cr.ObjectMeta.Name) - r.Fields = Fields{ + r.Fields = model1.Fields{ cr.Name, mapToStr(cr.Labels), - toAge(cr.GetCreationTimestamp()), + ToAge(cr.GetCreationTimestamp()), } return nil diff --git a/internal/render/cr_test.go b/internal/render/cr_test.go index 32492370b3..d6d175311a 100644 --- a/internal/render/cr_test.go +++ b/internal/render/cr_test.go @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestClusterRoleRender(t *testing.T) { c := render.ClusterRole{} - r := render.NewRow(2) + r := model1.NewRow(2) assert.NoError(t, c.Render(load(t, "cr"), "-", &r)) assert.Equal(t, "-/blee", r.ID) - assert.Equal(t, render.Fields{"blee"}, r.Fields[:1]) + assert.Equal(t, model1.Fields{"blee"}, r.Fields[:1]) } diff --git a/internal/render/crb.go b/internal/render/crb.go index 89873e98af..8290973e6f 100644 --- a/internal/render/crb.go +++ b/internal/render/crb.go @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( "fmt" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -15,22 +19,22 @@ type ClusterRoleBinding struct { } // Header returns a header rbw. -func (ClusterRoleBinding) Header(string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "CLUSTERROLE"}, - HeaderColumn{Name: "SUBJECT-KIND"}, - HeaderColumn{Name: "SUBJECTS"}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (ClusterRoleBinding) Header(string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "CLUSTERROLE"}, + model1.HeaderColumn{Name: "SUBJECT-KIND"}, + model1.HeaderColumn{Name: "SUBJECTS"}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (ClusterRoleBinding) Render(o interface{}, ns string, r *Row) error { +func (ClusterRoleBinding) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected ClusterRoleBinding, but got %T", o) + return fmt.Errorf("expected ClusterRoleBinding, but got %T", o) } var crb rbacv1.ClusterRoleBinding err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &crb) @@ -41,13 +45,13 @@ func (ClusterRoleBinding) Render(o interface{}, ns string, r *Row) error { kind, ss := renderSubjects(crb.Subjects) r.ID = client.FQN("-", crb.ObjectMeta.Name) - r.Fields = Fields{ + r.Fields = model1.Fields{ crb.Name, crb.RoleRef.Name, kind, ss, mapToStr(crb.Labels), - toAge(crb.GetCreationTimestamp()), + ToAge(crb.GetCreationTimestamp()), } return nil diff --git a/internal/render/crb_test.go b/internal/render/crb_test.go index 931046e8f4..4b350a4c94 100644 --- a/internal/render/crb_test.go +++ b/internal/render/crb_test.go @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestClusterRoleBindingRender(t *testing.T) { c := render.ClusterRoleBinding{} - r := render.NewRow(5) + r := model1.NewRow(5) assert.NoError(t, c.Render(load(t, "crb"), "-", &r)) assert.Equal(t, "-/blee", r.ID) - assert.Equal(t, render.Fields{"blee", "blee", "User", "fernand"}, r.Fields[:4]) + assert.Equal(t, model1.Fields{"blee", "blee", "User", "fernand"}, r.Fields[:4]) } diff --git a/internal/render/crd.go b/internal/render/crd.go index 0e6c2b0048..fddeec1973 100644 --- a/internal/render/crd.go +++ b/internal/render/crd.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -6,6 +9,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/rs/zerolog/log" v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -18,21 +22,25 @@ type CustomResourceDefinition struct { } // Header returns a header rbw. -func (CustomResourceDefinition) Header(string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "VERSIONS"}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (CustomResourceDefinition) Header(string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "GROUP"}, + model1.HeaderColumn{Name: "KIND"}, + model1.HeaderColumn{Name: "VERSIONS"}, + model1.HeaderColumn{Name: "SCOPE"}, + model1.HeaderColumn{Name: "ALIASES", Wide: true}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (c CustomResourceDefinition) Render(o interface{}, ns string, r *Row) error { +func (c CustomResourceDefinition) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected CustomResourceDefinition, but got %T", o) + return fmt.Errorf("expected CustomResourceDefinition, but got %T", o) } var crd v1.CustomResourceDefinition @@ -41,7 +49,7 @@ func (c CustomResourceDefinition) Render(o interface{}, ns string, r *Row) error return err } - versions := make([]string, 0, 3) + versions := make([]string, 0, len(crd.Spec.Versions)) for _, v := range crd.Spec.Versions { if v.Served { n := v.Name @@ -52,16 +60,20 @@ func (c CustomResourceDefinition) Render(o interface{}, ns string, r *Row) error } } if len(versions) == 0 { - log.Warn().Msgf("unable to assert CRD versions for %s", crd.GetName()) + log.Warn().Msgf("unable to assert CRD versions for %s", crd.Name) } - r.ID = client.FQN(client.ClusterScope, crd.GetName()) - r.Fields = Fields{ - crd.GetName(), + r.ID = client.MetaFQN(crd.ObjectMeta) + r.Fields = model1.Fields{ + crd.Spec.Names.Plural, + crd.Spec.Group, + crd.Spec.Names.Kind, naStrings(versions), + string(crd.Spec.Scope), + naStrings(crd.Spec.Names.ShortNames), mapToIfc(crd.GetLabels()), - asStatus(c.diagnose(crd.GetName(), crd.Spec.Versions)), - toAge(crd.GetCreationTimestamp()), + AsStatus(c.diagnose(crd.Name, crd.Spec.Versions)), + ToAge(crd.GetCreationTimestamp()), } return nil @@ -84,7 +96,7 @@ func (c CustomResourceDefinition) diagnose(n string, vv []v1.CustomResourceDefin if v.DeprecationWarning != nil { ee = append(ee, fmt.Errorf("%s", *v.DeprecationWarning)) } else { - ee = append(ee, fmt.Errorf("%s[%s] is deprecated!", n, v.Name)) + ee = append(ee, fmt.Errorf("%s[%s] is deprecated", n, v.Name)) } } } diff --git a/internal/render/crd_test.go b/internal/render/crd_test.go index 0bd9d18634..a88715ee47 100644 --- a/internal/render/crd_test.go +++ b/internal/render/crd_test.go @@ -1,17 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestCustomResourceDefinitionRender(t *testing.T) { c := render.CustomResourceDefinition{} - r := render.NewRow(2) + r := model1.NewRow(2) assert.NoError(t, c.Render(load(t, "crd"), "", &r)) assert.Equal(t, "-/adapters.config.istio.io", r.ID) - assert.Equal(t, render.Fields{"adapters.config.istio.io"}, r.Fields[:1]) + assert.Equal(t, "adapters", r.Fields[0]) + assert.Equal(t, "config.istio.io", r.Fields[1]) } diff --git a/internal/render/cronjob.go b/internal/render/cronjob.go index 8dc08a2e4d..bd1ce7c334 100644 --- a/internal/render/cronjob.go +++ b/internal/render/cronjob.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -6,6 +9,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -18,28 +22,29 @@ type CronJob struct { } // Header returns a header row. -func (CronJob) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "SCHEDULE"}, - HeaderColumn{Name: "SUSPEND"}, - HeaderColumn{Name: "ACTIVE"}, - HeaderColumn{Name: "LAST_SCHEDULE", Time: true}, - HeaderColumn{Name: "SELECTOR", Wide: true}, - HeaderColumn{Name: "CONTAINERS", Wide: true}, - HeaderColumn{Name: "IMAGES", Wide: true}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (CronJob) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", VS: true}, + model1.HeaderColumn{Name: "SCHEDULE"}, + model1.HeaderColumn{Name: "SUSPEND"}, + model1.HeaderColumn{Name: "ACTIVE"}, + model1.HeaderColumn{Name: "LAST_SCHEDULE", Time: true}, + model1.HeaderColumn{Name: "SELECTOR", Wide: true}, + model1.HeaderColumn{Name: "CONTAINERS", Wide: true}, + model1.HeaderColumn{Name: "IMAGES", Wide: true}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (c CronJob) Render(o interface{}, ns string, r *Row) error { +func (c CronJob) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected CronJob, but got %T", o) + return fmt.Errorf("expected CronJob, but got %T", o) } var cj batchv1.CronJob err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &cj) @@ -49,13 +54,14 @@ func (c CronJob) Render(o interface{}, ns string, r *Row) error { lastScheduled := "" if cj.Status.LastScheduleTime != nil { - lastScheduled = toAge(*cj.Status.LastScheduleTime) + lastScheduled = ToAge(*cj.Status.LastScheduleTime) } r.ID = client.MetaFQN(cj.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ cj.Namespace, cj.Name, + computeVulScore(cj.ObjectMeta, &cj.Spec.JobTemplate.Spec.Template.Spec), cj.Spec.Schedule, boolPtrToStr(cj.Spec.Suspend), strconv.Itoa(len(cj.Status.Active)), @@ -65,7 +71,7 @@ func (c CronJob) Render(o interface{}, ns string, r *Row) error { podImageNames(cj.Spec.JobTemplate.Spec.Template.Spec, true), mapToStr(cj.Labels), "", - toAge(cj.GetCreationTimestamp()), + ToAge(cj.GetCreationTimestamp()), } return nil diff --git a/internal/render/cronjob_test.go b/internal/render/cronjob_test.go index 0e3f86792e..34a77a96ce 100644 --- a/internal/render/cronjob_test.go +++ b/internal/render/cronjob_test.go @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestCronJobRender(t *testing.T) { c := render.CronJob{} - r := render.NewRow(6) + r := model1.NewRow(6) assert.NoError(t, c.Render(load(t, "cj"), "", &r)) assert.Equal(t, "default/hello", r.ID) - assert.Equal(t, render.Fields{"default", "hello", "*/1 * * * *", "false", "0"}, r.Fields[:5]) + assert.Equal(t, model1.Fields{"default", "hello", "0", "*/1 * * * *", "false", "0"}, r.Fields[:6]) } diff --git a/internal/render/delta_test.go b/internal/render/delta_test.go deleted file mode 100644 index c8140d7f1c..0000000000 --- a/internal/render/delta_test.go +++ /dev/null @@ -1,263 +0,0 @@ -package render_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/render" - "github.com/stretchr/testify/assert" -) - -func TestDeltaLabelize(t *testing.T) { - uu := map[string]struct { - o render.Row - n render.Row - e render.DeltaRow - }{ - "same": { - o: render.Row{ - Fields: render.Fields{"a", "b", "blee=fred,doh=zorg"}, - }, - n: render.Row{ - Fields: render.Fields{"a", "b", "blee=fred1,doh=zorg"}, - }, - e: render.DeltaRow{"", "", "fred", "zorg"}, - }, - } - - hh := render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - } - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - d := render.NewDeltaRow(u.o, u.n, hh) - d = d.Labelize([]int{0, 1}, 2) - assert.Equal(t, u.e, d) - }) - } -} - -func TestDeltaCustomize(t *testing.T) { - uu := map[string]struct { - r1, r2 render.Row - cols []int - e render.DeltaRow - }{ - "same": { - r1: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - r2: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - cols: []int{0, 1, 2}, - e: render.DeltaRow{"", "", ""}, - }, - "empty": { - r1: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - r2: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - e: render.DeltaRow{}, - }, - "diff-full": { - r1: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - r2: render.Row{ - Fields: render.Fields{"a1", "b1", "c1"}, - }, - cols: []int{0, 1, 2}, - e: render.DeltaRow{"a", "b", "c"}, - }, - "diff-reverse": { - r1: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - r2: render.Row{ - Fields: render.Fields{"a1", "b1", "c1"}, - }, - cols: []int{2, 1, 0}, - e: render.DeltaRow{"c", "b", "a"}, - }, - "diff-skip": { - r1: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - r2: render.Row{ - Fields: render.Fields{"a1", "b1", "c1"}, - }, - cols: []int{2, 0}, - e: render.DeltaRow{"c", "a"}, - }, - "diff-missing": { - r1: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - r2: render.Row{ - Fields: render.Fields{"a1", "b1", "c1"}, - }, - cols: []int{2, 10, 0}, - e: render.DeltaRow{"c", "", "a"}, - }, - "diff-negative": { - r1: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - r2: render.Row{ - Fields: render.Fields{"a1", "b1", "c1"}, - }, - cols: []int{2, -1, 0}, - e: render.DeltaRow{"c", "", "a"}, - }, - } - - hh := render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - } - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - d := render.NewDeltaRow(u.r1, u.r2, hh) - out := make(render.DeltaRow, len(u.cols)) - d.Customize(u.cols, out) - assert.Equal(t, u.e, out) - }) - } -} - -func TestDeltaNew(t *testing.T) { - uu := map[string]struct { - o render.Row - n render.Row - blank bool - e render.DeltaRow - }{ - "same": { - o: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - n: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - blank: true, - e: render.DeltaRow{"", "", ""}, - }, - "diff": { - o: render.Row{ - Fields: render.Fields{"a1", "b", "c"}, - }, - n: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - e: render.DeltaRow{"a1", "", ""}, - }, - "diff2": { - o: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - n: render.Row{ - Fields: render.Fields{"a", "b1", "c"}, - }, - e: render.DeltaRow{"", "b", ""}, - }, - "diffLast": { - o: render.Row{ - Fields: render.Fields{"a", "b", "c"}, - }, - n: render.Row{ - Fields: render.Fields{"a", "b", "c1"}, - }, - e: render.DeltaRow{"", "", "c"}, - }, - } - - hh := render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - } - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - d := render.NewDeltaRow(u.o, u.n, hh) - assert.Equal(t, u.e, d) - assert.Equal(t, u.blank, d.IsBlank()) - }) - } -} - -func TestDeltaBlank(t *testing.T) { - uu := map[string]struct { - r render.DeltaRow - e bool - }{ - "empty": { - r: render.DeltaRow{}, - e: true, - }, - "blank": { - r: render.DeltaRow{"", "", ""}, - e: true, - }, - "notblank": { - r: render.DeltaRow{"", "", "z"}, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.r.IsBlank()) - }) - } -} - -func TestDeltaDiff(t *testing.T) { - uu := map[string]struct { - d1, d2 render.DeltaRow - ageCol int - e bool - }{ - "empty": { - d1: render.DeltaRow{"f1", "f2", "f3"}, - ageCol: 2, - e: true, - }, - "same": { - d1: render.DeltaRow{"f1", "f2", "f3"}, - d2: render.DeltaRow{"f1", "f2", "f3"}, - ageCol: -1, - }, - "diff": { - d1: render.DeltaRow{"f1", "f2", "f3"}, - d2: render.DeltaRow{"f1", "f2", "f13"}, - ageCol: -1, - e: true, - }, - "diff-age-first": { - d1: render.DeltaRow{"f1", "f2", "f3"}, - d2: render.DeltaRow{"f1", "f2", "f13"}, - ageCol: 0, - e: true, - }, - "diff-age-last": { - d1: render.DeltaRow{"f1", "f2", "f3"}, - d2: render.DeltaRow{"f1", "f2", "f13"}, - ageCol: 2, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.d1.Diff(u.d2, u.ageCol)) - }) - } -} diff --git a/internal/render/dir.go b/internal/render/dir.go index 6c50c17d04..8e076d4e2e 100644 --- a/internal/render/dir.go +++ b/internal/render/dir.go @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( "fmt" "os" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -18,22 +22,22 @@ func (Dir) IsGeneric() bool { } // ColorerFunc colors a resource row. -func (Dir) ColorerFunc() ColorerFunc { - return func(ns string, _ Header, re RowEvent) tcell.Color { +func (Dir) ColorerFunc() model1.ColorerFunc { + return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color { return tcell.ColorCadetBlue } } // Header returns a header row. -func (Dir) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, +func (Dir) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, } } // Render renders a K8s resource to screen. // BOZO!! Pass in a row with pre-alloc fields?? -func (Dir) Render(o interface{}, ns string, r *Row) error { +func (Dir) Render(o interface{}, ns string, r *model1.Row) error { d, ok := o.(DirRes) if !ok { return fmt.Errorf("expected DirRes, but got %T", o) diff --git a/internal/render/dp.go b/internal/render/dp.go index 88841df5be..1444eeb99a 100644 --- a/internal/render/dp.go +++ b/internal/render/dp.go @@ -1,10 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( "fmt" "strconv" + "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" + "github.com/derailed/tcell/v2" "github.com/derailed/tview" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -17,29 +23,44 @@ type Deployment struct { } // ColorerFunc colors a resource row. -func (d Deployment) ColorerFunc() ColorerFunc { - return DefaultColorer +func (d Deployment) ColorerFunc() model1.ColorerFunc { + return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { + c := model1.DefaultColorer(ns, h, re) + + idx, ok := h.IndexOf("READY", true) + if !ok { + return c + } + ready := strings.TrimSpace(re.Row.Fields[idx]) + tt := strings.Split(ready, "/") + if len(tt) == 2 && tt[1] == "0" { + return model1.PendingColor + } + + return c + } } // Header returns a header row. -func (Deployment) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "READY", Align: tview.AlignRight}, - HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight}, - HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (Deployment) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", VS: true}, + model1.HeaderColumn{Name: "READY", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (d Deployment) Render(o interface{}, ns string, r *Row) error { +func (d Deployment) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected Deployment, but got %T", o) + return fmt.Errorf("expected Deployment, but got %T", o) } var dp appsv1.Deployment @@ -49,15 +70,16 @@ func (d Deployment) Render(o interface{}, ns string, r *Row) error { } r.ID = client.MetaFQN(dp.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ dp.Namespace, dp.Name, + computeVulScore(dp.ObjectMeta, &dp.Spec.Template.Spec), strconv.Itoa(int(dp.Status.AvailableReplicas)) + "/" + strconv.Itoa(int(dp.Status.Replicas)), strconv.Itoa(int(dp.Status.UpdatedReplicas)), strconv.Itoa(int(dp.Status.AvailableReplicas)), mapToStr(dp.Labels), - asStatus(d.diagnose(dp.Status.Replicas, dp.Status.AvailableReplicas)), - toAge(dp.GetCreationTimestamp()), + AsStatus(d.diagnose(dp.Status.Replicas, dp.Status.AvailableReplicas)), + ToAge(dp.GetCreationTimestamp()), } return nil @@ -67,5 +89,6 @@ func (Deployment) diagnose(desired, avail int32) error { if desired != avail { return fmt.Errorf("desiring %d replicas got %d available", desired, avail) } + return nil } diff --git a/internal/render/dp_test.go b/internal/render/dp_test.go index ea8168c6a0..e4ecc4b145 100644 --- a/internal/render/dp_test.go +++ b/internal/render/dp_test.go @@ -1,24 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestDpRender(t *testing.T) { c := render.Deployment{} - r := render.NewRow(7) + r := model1.NewRow(7) assert.Nil(t, c.Render(load(t, "dp"), "", &r)) assert.Equal(t, "icx/icx-db", r.ID) - assert.Equal(t, render.Fields{"icx", "icx-db", "1/1", "1", "1"}, r.Fields[:5]) + assert.Equal(t, model1.Fields{"icx", "icx-db", "0", "1/1", "1", "1"}, r.Fields[:6]) } func BenchmarkDpRender(b *testing.B) { c := render.Deployment{} - r := render.NewRow(7) + r := model1.NewRow(7) o := load(b, "dp") b.ResetTimer() diff --git a/internal/render/ds.go b/internal/render/ds.go index 80a3bff2e3..b3f047aa7a 100644 --- a/internal/render/ds.go +++ b/internal/render/ds.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -5,6 +8,7 @@ import ( "strconv" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tview" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -17,26 +21,27 @@ type DaemonSet struct { } // Header returns a header row. -func (DaemonSet) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "DESIRED", Align: tview.AlignRight}, - HeaderColumn{Name: "CURRENT", Align: tview.AlignRight}, - HeaderColumn{Name: "READY", Align: tview.AlignRight}, - HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight}, - HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (DaemonSet) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", VS: true}, + model1.HeaderColumn{Name: "DESIRED", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "CURRENT", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "READY", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "UP-TO-DATE", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (d DaemonSet) Render(o interface{}, ns string, r *Row) error { +func (d DaemonSet) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected DaemonSet, but got %T", o) + return fmt.Errorf("expected DaemonSet, but got %T", o) } var ds appsv1.DaemonSet err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ds) @@ -45,17 +50,18 @@ func (d DaemonSet) Render(o interface{}, ns string, r *Row) error { } r.ID = client.MetaFQN(ds.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ ds.Namespace, ds.Name, + computeVulScore(ds.ObjectMeta, &ds.Spec.Template.Spec), strconv.Itoa(int(ds.Status.DesiredNumberScheduled)), strconv.Itoa(int(ds.Status.CurrentNumberScheduled)), strconv.Itoa(int(ds.Status.NumberReady)), strconv.Itoa(int(ds.Status.UpdatedNumberScheduled)), strconv.Itoa(int(ds.Status.NumberAvailable)), mapToStr(ds.Labels), - asStatus(d.diagnose(ds.Status.DesiredNumberScheduled, ds.Status.NumberReady)), - toAge(ds.GetCreationTimestamp()), + AsStatus(d.diagnose(ds.Status.DesiredNumberScheduled, ds.Status.NumberReady)), + ToAge(ds.GetCreationTimestamp()), } return nil diff --git a/internal/render/ds_test.go b/internal/render/ds_test.go index 4ab3796395..16598332da 100644 --- a/internal/render/ds_test.go +++ b/internal/render/ds_test.go @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestDaemonSetRender(t *testing.T) { c := render.DaemonSet{} - r := render.NewRow(9) + r := model1.NewRow(9) assert.NoError(t, c.Render(load(t, "ds"), "", &r)) assert.Equal(t, "kube-system/fluentd-gcp-v3.2.0", r.ID) - assert.Equal(t, render.Fields{"kube-system", "fluentd-gcp-v3.2.0", "2", "2", "2", "2", "2"}, r.Fields[:7]) + assert.Equal(t, model1.Fields{"kube-system", "fluentd-gcp-v3.2.0", "0", "2", "2", "2", "2", "2"}, r.Fields[:8]) } diff --git a/internal/render/ep.go b/internal/render/ep.go index bb8c23c1b1..9fa4bcc80d 100644 --- a/internal/render/ep.go +++ b/internal/render/ep.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -6,6 +9,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -17,20 +21,20 @@ type Endpoints struct { } // Header returns a header row. -func (Endpoints) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "ENDPOINTS"}, - HeaderColumn{Name: "AGE", Time: true}, +func (Endpoints) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "ENDPOINTS"}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (e Endpoints) Render(o interface{}, ns string, r *Row) error { +func (e Endpoints) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected Endpoints, but got %T", o) + return fmt.Errorf("expected Endpoints, but got %T", o) } var ep v1.Endpoints err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ep) @@ -39,12 +43,12 @@ func (e Endpoints) Render(o interface{}, ns string, r *Row) error { } r.ID = client.MetaFQN(ep.ObjectMeta) - r.Fields = make(Fields, 0, len(e.Header(ns))) - r.Fields = Fields{ + r.Fields = make(model1.Fields, 0, len(e.Header(ns))) + r.Fields = model1.Fields{ ep.Namespace, ep.Name, missing(toEPs(ep.Subsets)), - toAge(ep.GetCreationTimestamp()), + ToAge(ep.GetCreationTimestamp()), } return nil diff --git a/internal/render/ep_test.go b/internal/render/ep_test.go index 2e1b0a10e3..f4359f3a7f 100644 --- a/internal/render/ep_test.go +++ b/internal/render/ep_test.go @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestEndpointsRender(t *testing.T) { c := render.Endpoints{} - r := render.NewRow(4) + r := model1.NewRow(4) assert.NoError(t, c.Render(load(t, "ep"), "", &r)) assert.Equal(t, "default/dictionary1", r.ID) - assert.Equal(t, render.Fields{"default", "dictionary1", ""}, r.Fields[:3]) + assert.Equal(t, model1.Fields{"default", "dictionary1", ""}, r.Fields[:3]) } diff --git a/internal/render/ev.go b/internal/render/ev.go index 018e899c3a..28e04f7923 100644 --- a/internal/render/ev.go +++ b/internal/render/ev.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -5,8 +8,9 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" - metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // Event renders a K8s Event to screen. @@ -19,14 +23,14 @@ func (*Event) IsGeneric() bool { } // ColorerFunc colors a resource row. -func (e *Event) ColorerFunc() ColorerFunc { - return func(ns string, h Header, re RowEvent) tcell.Color { - reasonCol := h.IndexOf("REASON", true) - if reasonCol >= 0 && strings.TrimSpace(re.Row.Fields[reasonCol]) == "Killing" { - return KillColor +func (e *Event) ColorerFunc() model1.ColorerFunc { + return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { + idx, ok := h.IndexOf("REASON", true) + if ok && strings.TrimSpace(re.Row.Fields[idx]) == "Killing" { + return model1.KillColor } - return DefaultColorer(ns, h, re) + return model1.DefaultColorer(ns, h, re) } } @@ -43,14 +47,14 @@ var wideCols = map[string]struct{}{ "MESSAGE": {}, } -func (e *Event) Header(ns string) Header { +func (e *Event) Header(ns string) model1.Header { if e.table == nil { - return Header{} + return model1.Header{} } - hh := make(Header, 0, len(e.table.ColumnDefinitions)) - hh = append(hh, HeaderColumn{Name: "NAMESPACE"}) + hh := make(model1.Header, 0, len(e.table.ColumnDefinitions)) + hh = append(hh, model1.HeaderColumn{Name: "NAMESPACE"}) for _, h := range e.table.ColumnDefinitions { - header := HeaderColumn{Name: strings.ToUpper(h.Name)} + header := model1.HeaderColumn{Name: strings.ToUpper(h.Name)} if _, ok := ageCols[header.Name]; ok { header.Time = true } @@ -64,8 +68,8 @@ func (e *Event) Header(ns string) Header { } // Render renders a K8s resource to screen. -func (e *Event) Render(o interface{}, ns string, r *Row) error { - row, ok := o.(metav1beta1.TableRow) +func (e *Event) Render(o interface{}, ns string, r *model1.Row) error { + row, ok := o.(metav1.TableRow) if !ok { return fmt.Errorf("expecting a TableRow but got %T", o) } @@ -78,7 +82,7 @@ func (e *Event) Render(o interface{}, ns string, r *Row) error { return fmt.Errorf("expecting row 0 to be a string but got %T", row.Cells[0]) } r.ID = client.FQN(nns, name) - r.Fields = make(Fields, 0, len(e.Header(ns))) + r.Fields = make(model1.Fields, 0, len(e.Header(ns))) r.Fields = append(r.Fields, nns) for _, o := range row.Cells { if o == nil { diff --git a/internal/render/ev_test.go b/internal/render/ev_test.go index e65137a153..dbb0794b66 100644 --- a/internal/render/ev_test.go +++ b/internal/render/ev_test.go @@ -1,19 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test // BOZO!! // func TestEventRender(t *testing.T) { // c := render.Event{} -// r := render.NewRow(7) +// r := model1.NewRow(7) // c.Render(load(t, "ev"), "", &r) // assert.Equal(t, "default/hello-1567197780-mn4mv.15bfce150bd764dd", r.ID) -// assert.Equal(t, render.Fields{"default", "pod:hello-1567197780-mn4mv", "Normal", "Pulled", "kubelet", "1", `Successfully pulled image "blang/busybox-bash"`}, r.Fields[:7]) +// assert.Equal(t, model1.Fields{"default", "pod:hello-1567197780-mn4mv", "Normal", "Pulled", "kubelet", "1", `Successfully pulled image "blang/busybox-bash"`}, r.Fields[:7]) // } // func BenchmarkEventRender(b *testing.B) { // ev := load(b, "ev") // var re render.Event -// r := render.NewRow(7) +// r := model1.NewRow(7) // b.ResetTimer() // b.ReportAllocs() diff --git a/internal/render/generic.go b/internal/render/generic.go index d049d204df..56f5523963 100644 --- a/internal/render/generic.go +++ b/internal/render/generic.go @@ -1,14 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( "encoding/json" "errors" "fmt" - "github.com/rs/zerolog/log" "strings" "github.com/derailed/k9s/internal/client" - metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "github.com/derailed/k9s/internal/model1" + "github.com/rs/zerolog/log" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ageTableCol = "Age" @@ -16,8 +20,8 @@ const ageTableCol = "Age" // Generic renders a generic resource to screen. type Generic struct { Base - table *metav1beta1.Table - header Header + table *metav1.Table + header model1.Header ageIndex int } @@ -26,45 +30,45 @@ func (*Generic) IsGeneric() bool { } // SetTable sets the tabular resource. -func (g *Generic) SetTable(ns string, t *metav1beta1.Table) { +func (g *Generic) SetTable(ns string, t *metav1.Table) { g.table = t g.header = g.Header(ns) } // ColorerFunc colors a resource row. -func (*Generic) ColorerFunc() ColorerFunc { - return DefaultColorer +func (*Generic) ColorerFunc() model1.ColorerFunc { + return model1.DefaultColorer } // Header returns a header row. -func (g *Generic) Header(ns string) Header { +func (g *Generic) Header(ns string) model1.Header { if g.header != nil { return g.header } if g.table == nil { - return Header{} + return model1.Header{} } - h := make(Header, 0, len(g.table.ColumnDefinitions)) + h := make(model1.Header, 0, len(g.table.ColumnDefinitions)) if !client.IsClusterScoped(ns) { - h = append(h, HeaderColumn{Name: "NAMESPACE"}) + h = append(h, model1.HeaderColumn{Name: "NAMESPACE"}) } for i, c := range g.table.ColumnDefinitions { if c.Name == ageTableCol { g.ageIndex = i continue } - h = append(h, HeaderColumn{Name: strings.ToUpper(c.Name)}) + h = append(h, model1.HeaderColumn{Name: strings.ToUpper(c.Name)}) } if g.ageIndex > 0 { - h = append(h, HeaderColumn{Name: "AGE", Time: true}) + h = append(h, model1.HeaderColumn{Name: "AGE", Time: true}) } return h } // Render renders a K8s resource to screen. -func (g *Generic) Render(o interface{}, ns string, r *Row) error { - row, ok := o.(metav1beta1.TableRow) +func (g *Generic) Render(o interface{}, ns string, r *model1.Row) error { + row, ok := o.(metav1.TableRow) if !ok { return fmt.Errorf("expecting a TableRow but got %T", o) } @@ -77,7 +81,7 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error { return fmt.Errorf("expecting row 0 to be a string but got %T", row.Cells[0]) } r.ID = client.FQN(nns, name) - r.Fields = make(Fields, 0, len(g.Header(ns))) + r.Fields = make(model1.Fields, 0, len(g.Header(ns))) if !client.IsClusterScoped(ns) { r.Fields = append(r.Fields, nns) } @@ -95,8 +99,9 @@ func (g *Generic) Render(o interface{}, ns string, r *Row) error { } if d, ok := duration.(string); ok { r.Fields = append(r.Fields, d) - } else { + } else if g.ageIndex > 0 { log.Warn().Msgf("No Duration detected on age field") + r.Fields = append(r.Fields, NAValue) } return nil diff --git a/internal/render/generic_test.go b/internal/render/generic_test.go index 7f3f488407..a71807622d 100644 --- a/internal/render/generic_test.go +++ b/internal/render/generic_test.go @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" @@ -15,65 +19,65 @@ func TestGenericRender(t *testing.T) { ns string table *metav1beta1.Table eID string - eFields render.Fields - eHeader render.Header + eFields model1.Fields + eHeader model1.Header }{ "withNS": { ns: "ns1", table: makeNSGeneric(), eID: "ns1/fred", - eFields: render.Fields{"ns1", "c1", "c2", "c3"}, - eHeader: render.Header{ - render.HeaderColumn{Name: "NAMESPACE"}, - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, + eFields: model1.Fields{"ns1", "c1", "c2", "c3"}, + eHeader: model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "C"}, }, }, "all": { ns: client.NamespaceAll, table: makeNSGeneric(), eID: "ns1/fred", - eFields: render.Fields{"ns1", "c1", "c2", "c3"}, - eHeader: render.Header{ - render.HeaderColumn{Name: "NAMESPACE"}, - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, + eFields: model1.Fields{"ns1", "c1", "c2", "c3"}, + eHeader: model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "C"}, }, }, "allNS": { - ns: client.AllNamespaces, + ns: client.NamespaceAll, table: makeNSGeneric(), eID: "ns1/fred", - eFields: render.Fields{"ns1", "c1", "c2", "c3"}, - eHeader: render.Header{ - render.HeaderColumn{Name: "NAMESPACE"}, - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, + eFields: model1.Fields{"ns1", "c1", "c2", "c3"}, + eHeader: model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "C"}, }, }, "clusterWide": { ns: client.ClusterScope, table: makeNoNSGeneric(), eID: "-/fred", - eFields: render.Fields{"c1", "c2", "c3"}, - eHeader: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, + eFields: model1.Fields{"c1", "c2", "c3"}, + eHeader: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "C"}, }, }, "age": { ns: client.ClusterScope, table: makeAgeGeneric(), eID: "-/fred", - eFields: render.Fields{"c1", "c2", "2d"}, - eHeader: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "C"}, - render.HeaderColumn{Name: "AGE", Time: true}, + eFields: model1.Fields{"c1", "c2", "2d"}, + eHeader: model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "C"}, + model1.HeaderColumn{Name: "AGE", Time: true}, }, }, } @@ -82,7 +86,7 @@ func TestGenericRender(t *testing.T) { var re render.Generic u := uu[k] t.Run(k, func(t *testing.T) { - var r render.Row + var r model1.Row re.SetTable(u.ns, u.table) assert.Equal(t, u.eHeader, re.Header(u.ns)) diff --git a/internal/render/helm.go b/internal/render/helm.go deleted file mode 100644 index 2d2e0d7707..0000000000 --- a/internal/render/helm.go +++ /dev/null @@ -1,94 +0,0 @@ -package render - -import ( - "fmt" - "strconv" - - "github.com/derailed/k9s/internal/client" - "github.com/derailed/tcell/v2" - "helm.sh/helm/v3/pkg/release" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -// Helm renders a helm chart to screen. -type Helm struct{} - -// IsGeneric identifies a generic handler. -func (Helm) IsGeneric() bool { - return false -} - -// ColorerFunc colors a resource row. -func (Helm) ColorerFunc() ColorerFunc { - return func(ns string, h Header, re RowEvent) tcell.Color { - if !Happy(ns, h, re.Row) { - return ErrColor - } - - return tcell.ColorMediumSpringGreen - } -} - -// Header returns a header row. -func (Helm) Header(_ string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "REVISION"}, - HeaderColumn{Name: "STATUS"}, - HeaderColumn{Name: "CHART"}, - HeaderColumn{Name: "APP VERSION"}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, - } -} - -// Render renders a chart to screen. -func (c Helm) Render(o interface{}, ns string, r *Row) error { - h, ok := o.(HelmRes) - if !ok { - return fmt.Errorf("expected HelmRes, but got %T", o) - } - - r.ID = client.FQN(h.Release.Namespace, h.Release.Name) - r.Fields = Fields{ - h.Release.Namespace, - h.Release.Name, - strconv.Itoa(h.Release.Version), - h.Release.Info.Status.String(), - h.Release.Chart.Metadata.Name + "-" + h.Release.Chart.Metadata.Version, - h.Release.Chart.Metadata.AppVersion, - asStatus(c.diagnose(h.Release.Info.Status.String())), - toAge(metav1.Time{Time: h.Release.Info.LastDeployed.Time}), - } - - return nil -} - -func (c Helm) diagnose(s string) error { - if s != "deployed" { - return fmt.Errorf("chart is in an invalid state") - } - - return nil -} - -// ---------------------------------------------------------------------------- -// Helpers... - -// HelmRes represents an helm chart resource. -type HelmRes struct { - Release *release.Release -} - -// GetObjectKind returns a schema object. -func (HelmRes) GetObjectKind() schema.ObjectKind { - return nil -} - -// DeepCopyObject returns a container copy. -func (h HelmRes) DeepCopyObject() runtime.Object { - return h -} diff --git a/internal/render/helm/chart.go b/internal/render/helm/chart.go new file mode 100644 index 0000000000..ee17b98a2a --- /dev/null +++ b/internal/render/helm/chart.go @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package helm + +import ( + "fmt" + "strconv" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" + "github.com/derailed/k9s/internal/render" + "helm.sh/helm/v3/pkg/release" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// Chart renders a helm chart to screen. +type Chart struct{} + +// IsGeneric identifies a generic handler. +func (Chart) IsGeneric() bool { + return false +} + +// ColorerFunc colors a resource row. +func (Chart) ColorerFunc() model1.ColorerFunc { + return model1.DefaultColorer +} + +// Header returns a header row. +func (Chart) Header(_ string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "REVISION"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "CHART"}, + model1.HeaderColumn{Name: "APP VERSION"}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, + } +} + +// Render renders a chart to screen. +func (c Chart) Render(o interface{}, ns string, r *model1.Row) error { + h, ok := o.(ReleaseRes) + if !ok { + return fmt.Errorf("expected ReleaseRes, but got %T", o) + } + + r.ID = client.FQN(h.Release.Namespace, h.Release.Name) + r.Fields = model1.Fields{ + h.Release.Namespace, + h.Release.Name, + strconv.Itoa(h.Release.Version), + h.Release.Info.Status.String(), + h.Release.Chart.Metadata.Name + "-" + h.Release.Chart.Metadata.Version, + h.Release.Chart.Metadata.AppVersion, + render.AsStatus(c.diagnose(h.Release.Info.Status.String())), + render.ToAge(metav1.Time{Time: h.Release.Info.LastDeployed.Time}), + } + + return nil +} + +func (c Chart) diagnose(s string) error { + if s != "deployed" { + return fmt.Errorf("chart is in an invalid state") + } + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +// ReleaseRes represents an helm chart resource. +type ReleaseRes struct { + Release *release.Release +} + +// GetObjectKind returns a schema object. +func (ReleaseRes) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (h ReleaseRes) DeepCopyObject() runtime.Object { + return h +} diff --git a/internal/render/helm/history.go b/internal/render/helm/history.go new file mode 100644 index 0000000000..cf0f118d33 --- /dev/null +++ b/internal/render/helm/history.go @@ -0,0 +1,69 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package helm + +import ( + "context" + "fmt" + "strconv" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" + "github.com/derailed/k9s/internal/render" +) + +// History renders a History chart to screen. +type History struct{} + +// Healthy checks component health. +func (History) Healthy(ctx context.Context, o interface{}) error { + return nil +} + +// IsGeneric identifies a generic handler. +func (History) IsGeneric() bool { + return false +} + +// ColorerFunc colors a resource row. +func (History) ColorerFunc() model1.ColorerFunc { + return model1.DefaultColorer +} + +// Header returns a header row. +func (History) Header(_ string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "REVISION"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "CHART"}, + model1.HeaderColumn{Name: "APP VERSION"}, + model1.HeaderColumn{Name: "DESCRIPTION"}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + } +} + +// Render renders a chart to screen. +func (c History) Render(o interface{}, ns string, r *model1.Row) error { + h, ok := o.(ReleaseRes) + if !ok { + return fmt.Errorf("expected HistoryRes, but got %T", o) + } + + r.ID = client.FQN(h.Release.Namespace, h.Release.Name) + r.ID += ":" + strconv.Itoa(h.Release.Version) + r.Fields = model1.Fields{ + strconv.Itoa(h.Release.Version), + h.Release.Info.Status.String(), + h.Release.Chart.Metadata.Name + "-" + h.Release.Chart.Metadata.Version, + h.Release.Chart.Metadata.AppVersion, + h.Release.Info.Description, + render.AsStatus(c.diagnose(h.Release.Info.Status.String())), + } + + return nil +} + +func (c History) diagnose(s string) error { + return nil +} diff --git a/internal/render/helpers.go b/internal/render/helpers.go index 95ed2cf6ef..522a6fdf71 100644 --- a/internal/render/helpers.go +++ b/internal/render/helpers.go @@ -1,22 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( + "context" "sort" "strconv" "strings" "time" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/vul" "github.com/derailed/tview" - runewidth "github.com/mattn/go-runewidth" + "github.com/mattn/go-runewidth" "github.com/rs/zerolog/log" "golang.org/x/text/language" "golang.org/x/text/message" - "k8s.io/apimachinery/pkg/api/resource" + v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/duration" ) +// ExtractImages returns a collection of container images. +// !!BOZO!! If this has any legs?? enable scans on other container types. +func ExtractImages(spec *v1.PodSpec) []string { + ii := make([]string, 0, len(spec.Containers)) + for _, c := range spec.Containers { + ii = append(ii, c.Image) + } + + return ii +} + +func computeVulScore(m metav1.ObjectMeta, spec *v1.PodSpec) string { + if vul.ImgScanner == nil || vul.ImgScanner.ShouldExcludes(m) { + return "0" + } + ii := ExtractImages(spec) + vul.ImgScanner.Enqueue(context.Background(), ii...) + + return vul.ImgScanner.Score(ii...) +} + func runesToNum(rr []rune) int64 { var r int64 var m int64 = 1 @@ -29,60 +55,14 @@ func runesToNum(rr []rune) int64 { return r } -func durationToSeconds(duration string) int64 { - if len(duration) == 0 { - return 0 - } - - num := make([]rune, 0, 5) - var n, m int64 - for _, r := range duration { - switch r { - case 'y': - m = 365 * 24 * 60 * 60 - case 'd': - m = 24 * 60 * 60 - case 'h': - m = 60 * 60 - case 'm': - m = 60 - case 's': - m = 1 - default: - num = append(num, r) - continue - } - n, num = n+runesToNum(num)*m, num[:0] - } - - return n -} - -func capacityToNumber(capacity string) int64 { - quantity := resource.MustParse(capacity) - return quantity.Value() -} - // AsThousands prints a number with thousand separator. func AsThousands(n int64) string { p := message.NewPrinter(language.English) return p.Sprintf("%d", n) } -// Happy returns true if resource is happy, false otherwise. -func Happy(ns string, h Header, r Row) bool { - if len(r.Fields) == 0 { - return true - } - validCol := h.IndexOf("VALID", true) - if validCol < 0 { - return true - } - - return strings.TrimSpace(r.Fields[validCol]) == "" -} - -func asStatus(err error) string { +// AsStatus returns error as string. +func AsStatus(err error) string { if err == nil { return "" } @@ -201,7 +181,8 @@ func boolToStr(b bool) string { } } -func toAge(t metav1.Time) string { +// ToAge converts time to human duration. +func ToAge(t metav1.Time) string { if t.IsZero() { return UnknownValue } @@ -311,15 +292,15 @@ func strPtrToStr(s *string) string { return *s } -// Check if string is in a string list. -func in(ll []string, s string) bool { - for _, l := range ll { - if l == s { - return true - } - } - return false -} +// // Check if string is in a string list. +// func in(ll []string, s string) bool { +// for _, l := range ll { +// if l == s { +// return true +// } +// } +// return false +// } // Pad a string up to the given length or truncates if greater than length. func Pad(s string, width int) string { @@ -334,29 +315,29 @@ func Pad(s string, width int) string { return s + strings.Repeat(" ", width-len(s)) } -// Converts labels string to map. -func labelize(labels string) map[string]string { - ll := strings.Split(labels, ",") - data := make(map[string]string, len(ll)) - - for _, l := range ll { - tokens := strings.Split(l, "=") - if len(tokens) == 2 { - data[tokens[0]] = tokens[1] - } - } - - return data -} - -func sortLabels(m map[string]string) (keys, vals []string) { - for k := range m { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - vals = append(vals, m[k]) - } - - return -} +// // Converts labels string to map. +// func labelize(labels string) map[string]string { +// ll := strings.Split(labels, ",") +// data := make(map[string]string, len(ll)) + +// for _, l := range ll { +// tokens := strings.Split(l, "=") +// if len(tokens) == 2 { +// data[tokens[0]] = tokens[1] +// } +// } + +// return data +// } + +// func sortLabels(m map[string]string) (keys, vals []string) { +// for k := range m { +// keys = append(keys, k) +// } +// sort.Strings(keys) +// for _, k := range keys { +// vals = append(vals, m[k]) +// } + +// return +// } diff --git a/internal/render/helpers_test.go b/internal/render/helpers_test.go index 7f8e2c0de1..0c6d0787d2 100644 --- a/internal/render/helpers_test.go +++ b/internal/render/helpers_test.go @@ -1,89 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( + "encoding/json" + "fmt" + "os" "testing" "time" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + "k8s.io/apimachinery/pkg/runtime" ) -func TestSortLabels(t *testing.T) { - uu := map[string]struct { - labels string - e [][]string - }{ - "simple": { - labels: "a=b,c=d", - e: [][]string{ - {"a", "c"}, - {"b", "d"}, - }, +func TestTableGenericHydrate(t *testing.T) { + raw := raw(t, "p1") + tt := metav1beta1.Table{ + ColumnDefinitions: []metav1beta1.TableColumnDefinition{ + {Name: "c1"}, + {Name: "c2"}, }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - hh, vv := sortLabels(labelize(u.labels)) - assert.Equal(t, u.e[0], hh) - assert.Equal(t, u.e[1], vv) - }) - } -} - -func TestLabelize(t *testing.T) { - uu := map[string]struct { - labels string - e map[string]string - }{ - "simple": { - labels: "a=b,c=d", - e: map[string]string{"a": "b", "c": "d"}, + Rows: []metav1beta1.TableRow{ + { + Cells: []interface{}{"fred", 10}, + Object: runtime.RawExtension{Raw: raw}, + }, + { + Cells: []interface{}{"blee", 20}, + Object: runtime.RawExtension{Raw: raw}, + }, }, } + rr := make([]model1.Row, 2) + var re Generic + re.SetTable("blee", &tt) - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, labelize(u.labels)) - }) - } + assert.Nil(t, model1.GenericHydrate("blee", &tt, rr, &re)) + assert.Equal(t, 2, len(rr)) + assert.Equal(t, 3, len(rr[0].Fields)) } -func TestDurationToSecond(t *testing.T) { - uu := map[string]struct { - s string - e int64 - }{ - "seconds": {s: "22s", e: 22}, - "minutes": {s: "22m", e: 1320}, - "hours": {s: "12h", e: 43200}, - "days": {s: "3d", e: 259200}, - "day_hour": {s: "3d9h", e: 291600}, - "day_hour_minute": {s: "2d22h3m", e: 252180}, - "day_hour_minute_seconds": {s: "2d22h3m50s", e: 252230}, - "year": {s: "3y", e: 94608000}, - "year_day": {s: "1y2d", e: 31708800}, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, durationToSeconds(u.s)) - }) +func TestTableHydrate(t *testing.T) { + oo := []runtime.Object{ + &PodWithMetrics{Raw: load(t, "p1")}, } -} - -func BenchmarkDurationToSecond(b *testing.B) { - t := "2d22h3m50s" + rr := make([]model1.Row, 1) - b.ReportAllocs() - b.ResetTimer() - for n := 0; n < b.N; n++ { - durationToSeconds(t) - } + assert.Nil(t, model1.Hydrate("blee", oo, rr, Pod{})) + assert.Equal(t, 1, len(rr)) + assert.Equal(t, 23, len(rr[0].Fields)) } func TestToAge(t *testing.T) { @@ -100,7 +71,7 @@ func TestToAge(t *testing.T) { for k := range uu { uc := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, uc.e, toAge(metav1.Time{Time: uc.t})) + assert.Equal(t, uc.e, ToAge(metav1.Time{Time: uc.t})) }) } } @@ -219,18 +190,33 @@ func TestNa(t *testing.T) { } func TestTruncate(t *testing.T) { - uu := []struct { - s string - l int - e string + uu := map[string]struct { + data string + size int + e string }{ - {"fred", 3, "fr…"}, - {"fred", 2, "f…"}, - {"fred", 10, "fred"}, + "same": { + data: "fred", + size: 4, + e: "fred", + }, + "small": { + data: "fred", + size: 10, + e: "fred", + }, + "larger": { + data: "fred", + size: 3, + e: "fr…", + }, } - for _, u := range uu { - assert.Equal(t, u.e, Truncate(u.s, u.l)) + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, Truncate(u.data, u.size)) + }) } } @@ -288,34 +274,6 @@ func TestBlank(t *testing.T) { } } -func TestIn(t *testing.T) { - uu := map[string]struct { - a []string - v string - e bool - }{ - "in": { - a: []string{"fred", "blee"}, - v: "blee", - e: true, - }, - "empty": { - v: "blee", - }, - "missing": { - a: []string{"fred", "blee"}, - v: "duh", - }, - } - - for k := range uu { - uc := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, uc.e, in(uc.a, uc.v)) - }) - } -} - func TestMetaFQN(t *testing.T) { uu := map[string]struct { m metav1.ObjectMeta @@ -469,3 +427,20 @@ func BenchmarkIntToStr(b *testing.B) { IntToStr(v) } } + +// Helpers... + +func load(t *testing.T, n string) *unstructured.Unstructured { + raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n)) + assert.Nil(t, err) + var o unstructured.Unstructured + err = json.Unmarshal(raw, &o) + assert.Nil(t, err) + return &o +} + +func raw(t *testing.T, n string) []byte { + raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.json", n)) + assert.Nil(t, err) + return raw +} diff --git a/internal/render/img_scan.go b/internal/render/img_scan.go new file mode 100644 index 0000000000..691e2f104a --- /dev/null +++ b/internal/render/img_scan.go @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package render + +import ( + "fmt" + "strings" + + "github.com/derailed/k9s/internal/model1" + "github.com/derailed/k9s/internal/vul" + "github.com/derailed/tcell/v2" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +const ( + CVEParseIdx = 5 + sevColName = "SEVERITY" +) + +// ImageScan renders scans report table. +type ImageScan struct { + Base +} + +// ColorerFunc colors a resource row. +func (c ImageScan) ColorerFunc() model1.ColorerFunc { + return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { + c := model1.DefaultColorer(ns, h, re) + + idx, ok := h.IndexOf(sevColName, true) + if !ok { + return c + } + sev := strings.TrimSpace(re.Row.Fields[idx]) + switch sev { + case vul.Sev1: + c = tcell.ColorRed + case vul.Sev2: + c = tcell.ColorDarkOrange + case vul.Sev3: + c = tcell.ColorYellow + case vul.Sev4: + c = tcell.ColorDeepSkyBlue + case vul.Sev5: + c = tcell.ColorCadetBlue + default: + c = tcell.ColorDarkOliveGreen + } + + return c + } + +} + +// Header returns a header row. +func (ImageScan) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "SEVERITY"}, + model1.HeaderColumn{Name: "VULNERABILITY"}, + model1.HeaderColumn{Name: "IMAGE"}, + model1.HeaderColumn{Name: "LIBRARY"}, + model1.HeaderColumn{Name: "VERSION"}, + model1.HeaderColumn{Name: "FIXED-IN"}, + model1.HeaderColumn{Name: "TYPE"}, + } +} + +// Render renders a K8s resource to screen. +func (is ImageScan) Render(o interface{}, name string, r *model1.Row) error { + res, ok := o.(ImageScanRes) + if !ok { + return fmt.Errorf("expected ImageScanRes, but got %T", o) + } + + r.ID = fmt.Sprintf("%s|%s", res.Image, strings.Join(res.Row, "|")) + r.Fields = model1.Fields{ + res.Row.Severity(), + res.Row.Vulnerability(), + res.Image, + res.Row.Name(), + res.Row.Version(), + res.Row.Fix(), + res.Row.Type(), + } + + return nil +} + +// ---------------------------------------------------------------------------- +// Helpers... + +// ImageScanRes represents a container and its metrics. +type ImageScanRes struct { + Image string + Row vul.Row +} + +// GetObjectKind returns a schema object. +func (ImageScanRes) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (is ImageScanRes) DeepCopyObject() runtime.Object { + return is +} diff --git a/internal/render/job.go b/internal/render/job.go index a8e4e5b176..7b25c49b17 100644 --- a/internal/render/job.go +++ b/internal/render/job.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -7,9 +10,9 @@ import ( "time" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/duration" @@ -21,25 +24,26 @@ type Job struct { } // Header returns a header row. -func (Job) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "COMPLETIONS"}, - HeaderColumn{Name: "DURATION"}, - HeaderColumn{Name: "SELECTOR", Wide: true}, - HeaderColumn{Name: "CONTAINERS", Wide: true}, - HeaderColumn{Name: "IMAGES", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (Job) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", VS: true}, + model1.HeaderColumn{Name: "COMPLETIONS"}, + model1.HeaderColumn{Name: "DURATION"}, + model1.HeaderColumn{Name: "SELECTOR", Wide: true}, + model1.HeaderColumn{Name: "CONTAINERS", Wide: true}, + model1.HeaderColumn{Name: "IMAGES", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (j Job) Render(o interface{}, ns string, r *Row) error { +func (j Job) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected Job, but got %T", o) + return fmt.Errorf("expected Job, but got %T", o) } var job batchv1.Job err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &job) @@ -51,28 +55,26 @@ func (j Job) Render(o interface{}, ns string, r *Row) error { cc, ii := toContainers(job.Spec.Template.Spec) r.ID = client.MetaFQN(job.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ job.Namespace, job.Name, + computeVulScore(job.ObjectMeta, &job.Spec.Template.Spec), ready, toDuration(job.Status), jobSelector(job.Spec), cc, ii, - asStatus(j.diagnose(ready, job.Status.CompletionTime)), - toAge(job.GetCreationTimestamp()), + AsStatus(j.diagnose(ready, job.Status)), + ToAge(job.GetCreationTimestamp()), } return nil } -func (Job) diagnose(ready string, completed *metav1.Time) error { - if completed == nil { - return nil - } +func (Job) diagnose(ready string, status batchv1.JobStatus) error { tokens := strings.Split(ready, "/") - if tokens[0] != tokens[1] { - return fmt.Errorf("expecting %s completion got %s", tokens[1], tokens[0]) + if tokens[0] != tokens[1] && status.Failed > 0 { + return fmt.Errorf("%d pods failed", status.Failed) } return nil } diff --git a/internal/render/job_test.go b/internal/render/job_test.go index e2a5fe5c03..028a4ddfe4 100644 --- a/internal/render/job_test.go +++ b/internal/render/job_test.go @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestJobRender(t *testing.T) { c := render.Job{} - r := render.NewRow(4) + r := model1.NewRow(4) assert.NoError(t, c.Render(load(t, "job"), "", &r)) assert.Equal(t, "default/hello-1567179180", r.ID) - assert.Equal(t, render.Fields{"default", "hello-1567179180", "1/1", "8s", "controller-uid=7473e6d0-cb3b-11e9-990f-42010a800218", "c1", "blang/busybox-bash"}, r.Fields[:7]) + assert.Equal(t, model1.Fields{"default", "hello-1567179180", "0", "1/1", "8s", "controller-uid=7473e6d0-cb3b-11e9-990f-42010a800218", "c1", "blang/busybox-bash"}, r.Fields[:8]) } diff --git a/internal/render/node.go b/internal/render/node.go index 2e892b104b..b207f61a2b 100644 --- a/internal/render/node.go +++ b/internal/render/node.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -8,6 +11,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tview" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -18,7 +22,7 @@ import ( const ( labelNodeRolePrefix = "node-role.kubernetes.io/" - nodeLabelRole = "kubernetes.io/role" + labelNodeRoleSuffix = "kubernetes.io/role" ) // Node renders a K8s Node to screen. @@ -27,30 +31,32 @@ type Node struct { } // Header returns a header row. -func (Node) Header(_ string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "STATUS"}, - HeaderColumn{Name: "ROLE"}, - HeaderColumn{Name: "VERSION"}, - HeaderColumn{Name: "KERNEL", Wide: true}, - HeaderColumn{Name: "INTERNAL-IP", Wide: true}, - HeaderColumn{Name: "EXTERNAL-IP", Wide: true}, - HeaderColumn{Name: "PODS", Align: tview.AlignRight}, - HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "%CPU", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "%MEM", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "CPU/A", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "MEM/A", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (Node) Header(string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "ROLE"}, + model1.HeaderColumn{Name: "ARCH", Wide: true}, + model1.HeaderColumn{Name: "TAINTS"}, + model1.HeaderColumn{Name: "VERSION"}, + model1.HeaderColumn{Name: "KERNEL", Wide: true}, + model1.HeaderColumn{Name: "INTERNAL-IP", Wide: true}, + model1.HeaderColumn{Name: "EXTERNAL-IP", Wide: true}, + model1.HeaderColumn{Name: "PODS", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "%CPU", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "%MEM", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "CPU/A", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "MEM/A", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (n Node) Render(o interface{}, ns string, r *Row) error { +func (n Node) Render(o interface{}, ns string, r *model1.Row) error { oo, ok := o.(*NodeWithMetrics) if !ok { return fmt.Errorf("expected *NodeAndMetrics, but got %T", o) @@ -82,10 +88,12 @@ func (n Node) Render(o interface{}, ns string, r *Row) error { podCount = NAValue } r.ID = client.FQN("", na) - r.Fields = Fields{ + r.Fields = model1.Fields{ no.Name, join(statuses, ","), join(roles, ","), + no.Status.NodeInfo.Architecture, + strconv.Itoa(len(no.Spec.Taints)), no.Status.NodeInfo.KubeletVersion, no.Status.NodeInfo.KernelVersion, iIP, @@ -98,8 +106,8 @@ func (n Node) Render(o interface{}, ns string, r *Row) error { toMc(a.cpu), toMi(a.mem), mapToStr(no.Labels), - asStatus(n.diagnose(statuses)), - toAge(no.GetCreationTimestamp()), + AsStatus(n.diagnose(statuses)), + ToAge(no.GetCreationTimestamp()), } return nil @@ -173,7 +181,7 @@ func nodeRoles(node *v1.Node, res []string) { res[index] = role index++ } - case k == nodeLabelRole && v != "": + case strings.HasSuffix(k, labelNodeRoleSuffix) && v != "": res[index] = v index++ } diff --git a/internal/render/node_test.go b/internal/render/node_test.go index 909868273b..09fb4a6889 100644 --- a/internal/render/node_test.go +++ b/internal/render/node_test.go @@ -1,8 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -16,13 +20,13 @@ func TestNodeRender(t *testing.T) { } var no render.Node - r := render.NewRow(14) + r := model1.NewRow(14) err := no.Render(&pom, "", &r) assert.Nil(t, err) assert.Equal(t, "minikube", r.ID) - e := render.Fields{"minikube", "Ready", "master", "v1.15.2", "4.15.0", "192.168.64.107", "", "0", "10", "20", "0", "0", "4000", "7874"} - assert.Equal(t, e, r.Fields[:14]) + e := model1.Fields{"minikube", "Ready", "master", "amd64", "0", "v1.15.2", "4.15.0", "192.168.64.107", "", "0", "10", "20", "0", "0", "4000", "7874"} + assert.Equal(t, e, r.Fields[:16]) } func BenchmarkNodeRender(b *testing.B) { @@ -31,7 +35,7 @@ func BenchmarkNodeRender(b *testing.B) { MX: makeNodeMX("n1", "10m", "10Mi"), } var no render.Node - r := render.NewRow(14) + r := model1.NewRow(14) b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { diff --git a/internal/render/np.go b/internal/render/np.go index 9275a21733..8f7bb24262 100644 --- a/internal/render/np.go +++ b/internal/render/np.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -5,6 +8,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" netv1 "k8s.io/api/networking/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -17,27 +21,27 @@ type NetworkPolicy struct { } // Header returns a header row. -func (NetworkPolicy) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "ING-SELECTOR", Wide: true}, - HeaderColumn{Name: "ING-PORTS"}, - HeaderColumn{Name: "ING-BLOCK"}, - HeaderColumn{Name: "EGR-SELECTOR", Wide: true}, - HeaderColumn{Name: "EGR-PORTS"}, - HeaderColumn{Name: "EGR-BLOCK"}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (NetworkPolicy) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "ING-SELECTOR", Wide: true}, + model1.HeaderColumn{Name: "ING-PORTS"}, + model1.HeaderColumn{Name: "ING-BLOCK"}, + model1.HeaderColumn{Name: "EGR-SELECTOR", Wide: true}, + model1.HeaderColumn{Name: "EGR-PORTS"}, + model1.HeaderColumn{Name: "EGR-BLOCK"}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (n NetworkPolicy) Render(o interface{}, ns string, r *Row) error { +func (n NetworkPolicy) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected NetworkPolicy, but got %T", o) + return fmt.Errorf("expected NetworkPolicy, but got %T", o) } var np netv1.NetworkPolicy err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &np) @@ -49,7 +53,7 @@ func (n NetworkPolicy) Render(o interface{}, ns string, r *Row) error { ep, es, eb := egress(np.Spec.Egress) r.ID = client.MetaFQN(np.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ np.Namespace, np.Name, is, @@ -60,7 +64,7 @@ func (n NetworkPolicy) Render(o interface{}, ns string, r *Row) error { eb, mapToStr(np.Labels), "", - toAge(np.GetCreationTimestamp()), + ToAge(np.GetCreationTimestamp()), } return nil diff --git a/internal/render/np_test.go b/internal/render/np_test.go index 2726e6c41e..bd371df453 100644 --- a/internal/render/np_test.go +++ b/internal/render/np_test.go @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestNetworkPolicyRender(t *testing.T) { c := render.NetworkPolicy{} - r := render.NewRow(9) + r := model1.NewRow(9) assert.NoError(t, c.Render(load(t, "np"), "", &r)) assert.Equal(t, "default/fred", r.ID) - assert.Equal(t, render.Fields{"default", "fred", "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[:8]) + assert.Equal(t, model1.Fields{"default", "fred", "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[:8]) } diff --git a/internal/render/ns.go b/internal/render/ns.go index 35a8d783cc..4f9ecf81f6 100644 --- a/internal/render/ns.go +++ b/internal/render/ns.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -6,6 +9,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -18,19 +22,17 @@ type Namespace struct { } // ColorerFunc colors a resource row. -func (n Namespace) ColorerFunc() ColorerFunc { - return func(ns string, h Header, re RowEvent) tcell.Color { - c := DefaultColorer(ns, h, re) - - if re.Kind == EventUpdate { - c = StdColor +func (n Namespace) ColorerFunc() model1.ColorerFunc { + return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { + c := model1.DefaultColorer(ns, h, re) + if c == model1.ErrColor { + return c } - if strings.Contains(strings.TrimSpace(re.Row.Fields[0]), "*") { - c = HighlightColor + if re.Kind == model1.EventUpdate { + c = model1.StdColor } - - if !Happy(ns, h, re.Row) { - c = ErrColor + if strings.Contains(strings.TrimSpace(re.Row.Fields[0]), "*") { + c = model1.HighlightColor } return c @@ -38,21 +40,21 @@ func (n Namespace) ColorerFunc() ColorerFunc { } // Header returns a header rbw. -func (Namespace) Header(string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "STATUS"}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (Namespace) Header(string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (n Namespace) Render(o interface{}, _ string, r *Row) error { +func (n Namespace) Render(o interface{}, _ string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected Namespace, but got %T", o) + return fmt.Errorf("expected Namespace, but got %T", o) } var ns v1.Namespace err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ns) @@ -61,12 +63,12 @@ func (n Namespace) Render(o interface{}, _ string, r *Row) error { } r.ID = client.MetaFQN(ns.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ ns.Name, string(ns.Status.Phase), mapToStr(ns.Labels), - asStatus(n.diagnose(ns.Status.Phase)), - toAge(ns.GetCreationTimestamp()), + AsStatus(n.diagnose(ns.Status.Phase)), + ToAge(ns.GetCreationTimestamp()), } return nil @@ -76,5 +78,6 @@ func (Namespace) diagnose(phase v1.NamespacePhase) error { if phase != v1.NamespaceActive && phase != v1.NamespaceTerminating { return errors.New("namespace not ready") } + return nil } diff --git a/internal/render/ns_test.go b/internal/render/ns_test.go index d83524564e..ad4337bd7d 100644 --- a/internal/render/ns_test.go +++ b/internal/render/ns_test.go @@ -1,8 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/derailed/tcell/v2" "github.com/stretchr/testify/assert" @@ -10,66 +14,66 @@ import ( func TestNSColorer(t *testing.T) { uu := map[string]struct { - re render.RowEvent + re model1.RowEvent e tcell.Color }{ "add": { - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{ + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{ "blee", "Active", }, }, }, - e: render.AddColor, + e: model1.AddColor, }, "update": { - re: render.RowEvent{ - Kind: render.EventUpdate, - Row: render.Row{ - Fields: render.Fields{ + re: model1.RowEvent{ + Kind: model1.EventUpdate, + Row: model1.Row{ + Fields: model1.Fields{ "blee", "Active", }, }, }, - e: render.StdColor, + e: model1.StdColor, }, "decorator": { - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{ + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{ "blee*", "Active", }, }, }, - e: render.HighlightColor, + e: model1.HighlightColor, }, } - h := render.Header{ - render.HeaderColumn{Name: "NAME"}, - render.HeaderColumn{Name: "STATUS"}, + h := model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "STATUS"}, } var r render.Namespace for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, r.ColorerFunc()("", h, u.re)) + assert.Equal(t, u.e, r.ColorerFunc()("", h, &u.re)) }) } } func TestNamespaceRender(t *testing.T) { c := render.Namespace{} - r := render.NewRow(3) + r := model1.NewRow(3) assert.NoError(t, c.Render(load(t, "ns"), "-", &r)) assert.Equal(t, "-/kube-system", r.ID) - assert.Equal(t, render.Fields{"kube-system", "Active"}, r.Fields[:2]) + assert.Equal(t, model1.Fields{"kube-system", "Active"}, r.Fields[:2]) } diff --git a/internal/render/ofaas.go b/internal/render/ofaas.go deleted file mode 100644 index a529b320d2..0000000000 --- a/internal/render/ofaas.go +++ /dev/null @@ -1,113 +0,0 @@ -package render - -// BOZO!! revamp with latest... - -// import ( -// "errors" -// "fmt" -// "strconv" -// "time" - -// "github.com/derailed/k9s/internal/client" -// "github.com/derailed/tview" -// "github.com/derailed/tcell/v2" - -// ofaas "github.com/openfaas/faas-provider/types" -// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -// "k8s.io/apimachinery/pkg/runtime" -// "k8s.io/apimachinery/pkg/runtime/schema" -// ) - -// const ( -// fnStatusReady = "Ready" -// fnStatusNotReady = "Not Ready" -// ) - -// // OpenFaas renders an openfaas function to screen. -// type OpenFaas struct{} - -// // ColorerFunc colors a resource row. -// func (o OpenFaas) ColorerFunc() ColorerFunc { -// return func(ns string, h Header, re RowEvent) tcell.Color { -// if !Happy(ns, h, re.Row) { -// return ErrColor -// } - -// return tcell.ColorPaleTurquoise -// } -// } - -// // Header returns a header row. -// func (OpenFaas) Header(ns string) Header { -// return Header{ -// HeaderColumn{Name: "NAMESPACE"}, -// HeaderColumn{Name: "NAME"}, -// HeaderColumn{Name: "STATUS"}, -// HeaderColumn{Name: "IMAGE"}, -// HeaderColumn{Name: "LABELS"}, -// HeaderColumn{Name: "INVOCATIONS", Align: tview.AlignRight}, -// HeaderColumn{Name: "REPLICAS", Align: tview.AlignRight}, -// HeaderColumn{Name: "AVAILABLE", Align: tview.AlignRight}, -// HeaderColumn{Name: "VALID", Wide: true}, -// HeaderColumn{Name: "AGE", Time: true}, -// } -// } - -// // Render renders a chart to screen. -// func (o OpenFaas) Render(i interface{}, ns string, r *Row) error { -// fn, ok := i.(OpenFaasRes) -// if !ok { -// return fmt.Errorf("expected OpenFaasRes, but got %T", o) -// } - -// var labels string -// if fn.Function.Labels != nil { -// labels = mapToStr(*fn.Function.Labels) -// } -// status := fnStatusReady -// if fn.Function.AvailableReplicas == 0 { -// status = fnStatusNotReady -// } - -// r.ID = client.FQN(fn.Function.Namespace, fn.Function.Name) -// r.Fields = Fields{ -// fn.Function.Namespace, -// fn.Function.Name, -// status, -// fn.Function.Image, -// labels, -// strconv.Itoa(int(fn.Function.InvocationCount)), -// strconv.Itoa(int(fn.Function.Replicas)), -// strconv.Itoa(int(fn.Function.AvailableReplicas)), -// asStatus(o.diagnose(status)), -// toAge(metav1.Time{Time: time.Now()}), -// } - -// return nil -// } - -// func (OpenFaas) diagnose(status string) error { -// if status != "Ready" { -// return errors.New("function not ready") -// } - -// return nil -// } - -// // ---------------------------------------------------------------------------- -// // Helpers... - -// // OpenFaasRes represents an openfaas function resource. -// type OpenFaasRes struct { -// Function ofaas.FunctionStatus `json:"function"` -// } - -// // GetObjectKind returns a schema object. -// func (OpenFaasRes) GetObjectKind() schema.ObjectKind { -// return nil -// } - -// // DeepCopyObject returns a container copy. -// func (h OpenFaasRes) DeepCopyObject() runtime.Object { -// return h -// } diff --git a/internal/render/ofaas_test.go b/internal/render/ofaas_test.go deleted file mode 100644 index 41222a1c0e..0000000000 --- a/internal/render/ofaas_test.go +++ /dev/null @@ -1,36 +0,0 @@ -package render_test - -// BOZO!! revamp with latest... - -// import ( -// "testing" - -// "github.com/derailed/k9s/internal/render" -// ofaas "github.com/openfaas/faas-provider/types" -// "github.com/stretchr/testify/assert" -// ) - -// func TestOpenFaasRender(t *testing.T) { -// c := render.OpenFaas{} -// r := render.NewRow(9) -// c.Render(makeFn("blee"), "", &r) - -// assert.Equal(t, "default/blee", r.ID) -// assert.Equal(t, render.Fields{"default", "blee", "Ready", "nginx:0", "fred=blee", "10", "1", "1"}, r.Fields[:8]) -// } - -// // Helpers... - -// func makeFn(n string) render.OpenFaasRes { -// return render.OpenFaasRes{ -// Function: ofaas.FunctionStatus{ -// Name: n, -// Namespace: "default", -// Image: "nginx:0", -// InvocationCount: 10, -// Replicas: 1, -// AvailableReplicas: 1, -// Labels: &map[string]string{"fred": "blee"}, -// }, -// } -// } diff --git a/internal/render/pdb.go b/internal/render/pdb.go index 24d4ccb9e5..f80fb4eef7 100644 --- a/internal/render/pdb.go +++ b/internal/render/pdb.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -5,8 +8,9 @@ import ( "strconv" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tview" - v1beta1 "k8s.io/api/policy/v1beta1" + v1 "k8s.io/api/policy/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" @@ -18,36 +22,36 @@ type PodDisruptionBudget struct { } // Header returns a header row. -func (PodDisruptionBudget) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "MIN AVAILABLE", Align: tview.AlignRight}, - HeaderColumn{Name: "MAX UNAVAILABLE", Align: tview.AlignRight}, - HeaderColumn{Name: "ALLOWED DISRUPTIONS", Align: tview.AlignRight}, - HeaderColumn{Name: "CURRENT", Align: tview.AlignRight}, - HeaderColumn{Name: "DESIRED", Align: tview.AlignRight}, - HeaderColumn{Name: "EXPECTED", Align: tview.AlignRight}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (PodDisruptionBudget) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "MIN AVAILABLE", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "MAX UNAVAILABLE", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "ALLOWED DISRUPTIONS", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "CURRENT", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "DESIRED", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "EXPECTED", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (p PodDisruptionBudget) Render(o interface{}, ns string, r *Row) error { +func (p PodDisruptionBudget) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected PodDisruptionBudget, but got %T", o) + return fmt.Errorf("expected PodDisruptionBudget, but got %T", o) } - var pdb v1beta1.PodDisruptionBudget + var pdb v1.PodDisruptionBudget err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &pdb) if err != nil { return err } r.ID = client.MetaFQN(pdb.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ pdb.Namespace, pdb.Name, numbToStr(pdb.Spec.MinAvailable), @@ -57,8 +61,8 @@ func (p PodDisruptionBudget) Render(o interface{}, ns string, r *Row) error { strconv.Itoa(int(pdb.Status.DesiredHealthy)), strconv.Itoa(int(pdb.Status.ExpectedPods)), mapToStr(pdb.Labels), - asStatus(p.diagnose(pdb.Spec.MinAvailable, pdb.Status.CurrentHealthy)), - toAge(pdb.GetCreationTimestamp()), + AsStatus(p.diagnose(pdb.Spec.MinAvailable, pdb.Status.CurrentHealthy)), + ToAge(pdb.GetCreationTimestamp()), } return nil diff --git a/internal/render/pdb_test.go b/internal/render/pdb_test.go index 7e14753b6e..9567c487f8 100644 --- a/internal/render/pdb_test.go +++ b/internal/render/pdb_test.go @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestPodDisruptionBudgetRender(t *testing.T) { c := render.PodDisruptionBudget{} - r := render.NewRow(9) + r := model1.NewRow(9) assert.NoError(t, c.Render(load(t, "pdb"), "", &r)) assert.Equal(t, "default/fred", r.ID) - assert.Equal(t, render.Fields{"default", "fred", "2", render.NAValue, "0", "0", "2", "0"}, r.Fields[:8]) + assert.Equal(t, model1.Fields{"default", "fred", "2", render.NAValue, "0", "0", "2", "0"}, r.Fields[:8]) } diff --git a/internal/render/pod.go b/internal/render/pod.go index afbcd7da81..829834df5a 100644 --- a/internal/render/pod.go +++ b/internal/render/pod.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -12,25 +15,35 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/kubernetes/pkg/util/node" mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" ) const ( - PhaseTerminating = "Terminating" - PhaseInitialized = "Initialized" - PhaseRunning = "Running" - PhaseNotReady = "NoReady" - PhaseCompleted = "Completed" - PhaseContainerCreating = "ContainerCreating" - PhasePodInitializing = "PodInitializing" - PhaseUnknown = "Unknown" - PhaseCrashLoop = "CrashLoopBackOff" - PhaseError = "Error" - PhaseImagePullBackOff = "ImagePullBackOff" - PhaseOOMKilled = "OOMKilled" + // NodeUnreachablePodReason is reason and message set on a pod when its state + // cannot be confirmed as kubelet is unresponsive on the node it is (was) running. + NodeUnreachablePodReason = "NodeLost" // k8s.io/kubernetes/pkg/util/node.NodeUnreachablePodReason + vulIdx = 2 +) + +const ( + PhaseTerminating = "Terminating" + PhaseInitialized = "Initialized" + PhaseRunning = "Running" + PhaseNotReady = "NoReady" + PhaseCompleted = "Completed" + PhaseContainerCreating = "ContainerCreating" + PhasePodInitializing = "PodInitializing" + PhaseUnknown = "Unknown" + PhaseCrashLoop = "CrashLoopBackOff" + PhaseError = "Error" + PhaseImagePullBackOff = "ImagePullBackOff" + PhaseOOMKilled = "OOMKilled" + PhasePending = "Pending" + PhaseContainerStatusUnknown = "ContainerStatusUnknown" + PhaseEvicted = "Evicted" ) // Pod renders a K8s Pod to screen. @@ -39,70 +52,67 @@ type Pod struct { } // ColorerFunc colors a resource row. -func (p Pod) ColorerFunc() ColorerFunc { - return func(ns string, h Header, re RowEvent) tcell.Color { - c := DefaultColorer(ns, h, re) +func (p Pod) ColorerFunc() model1.ColorerFunc { + return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { + c := model1.DefaultColorer(ns, h, re) - statusCol := h.IndexOf("STATUS", true) - if statusCol == -1 { + idx, ok := h.IndexOf("STATUS", true) + if !ok { return c } - status := strings.TrimSpace(re.Row.Fields[statusCol]) + status := strings.TrimSpace(re.Row.Fields[idx]) switch status { - case Pending: - c = PendingColor - case ContainerCreating, PodInitializing: - c = AddColor + case Pending, ContainerCreating: + c = model1.PendingColor + case PodInitializing: + c = model1.AddColor case Initialized: - c = HighlightColor + c = model1.HighlightColor case Completed: - c = CompletedColor + c = model1.CompletedColor case Running: - c = StdColor - if !Happy(ns, h, re.Row) { - c = ErrColor + if c != model1.ErrColor { + c = model1.StdColor } case Terminating: - c = KillColor - default: - if !Happy(ns, h, re.Row) { - c = ErrColor - } + c = model1.KillColor } + return c } } // Header returns a header row. -func (Pod) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "PF"}, - HeaderColumn{Name: "READY"}, - HeaderColumn{Name: "STATUS"}, - HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight}, - HeaderColumn{Name: "IP"}, - HeaderColumn{Name: "NODE"}, - HeaderColumn{Name: "NOMINATED NODE", Wide: true}, - HeaderColumn{Name: "READINESS GATES", Wide: true}, - HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "CPU/R:L", Align: tview.AlignRight, Wide: true}, - HeaderColumn{Name: "MEM/R:L", Align: tview.AlignRight, Wide: true}, - HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "%CPU/L", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "%MEM/R", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "%MEM/L", Align: tview.AlignRight, MX: true}, - HeaderColumn{Name: "QOS", Wide: true}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (p Pod) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", VS: true}, + model1.HeaderColumn{Name: "PF"}, + model1.HeaderColumn{Name: "READY"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "RESTARTS", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "CPU", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "MEM", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "CPU/R:L", Align: tview.AlignRight, Wide: true}, + model1.HeaderColumn{Name: "MEM/R:L", Align: tview.AlignRight, Wide: true}, + model1.HeaderColumn{Name: "%CPU/R", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "%CPU/L", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "%MEM/R", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "%MEM/L", Align: tview.AlignRight, MX: true}, + model1.HeaderColumn{Name: "IP"}, + model1.HeaderColumn{Name: "NODE"}, + model1.HeaderColumn{Name: "NOMINATED NODE", Wide: true}, + model1.HeaderColumn{Name: "READINESS GATES", Wide: true}, + model1.HeaderColumn{Name: "QOS", Wide: true}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (p Pod) Render(o interface{}, ns string, row *Row) error { +func (p Pod) Render(o interface{}, ns string, row *model1.Row) error { pwm, ok := o.(*PodWithMetrics) if !ok { return fmt.Errorf("expected PodWithMetrics, but got %T", o) @@ -113,23 +123,27 @@ func (p Pod) Render(o interface{}, ns string, row *Row) error { return err } - ss := po.Status.ContainerStatuses - cr, _, rc := p.Statuses(ss) + ics := po.Status.InitContainerStatuses + _, _, irc := p.Statuses(ics) + cs := po.Status.ContainerStatuses + cr, _, rc := p.Statuses(cs) - c, r := p.gatherPodMX(&po, pwm.MX) + var ccmx []mv1beta1.ContainerMetrics + if pwm.MX != nil { + ccmx = pwm.MX.Containers + } + c, r := gatherCoMX(po.Spec.Containers, ccmx) phase := p.Phase(&po) row.ID = client.MetaFQN(po.ObjectMeta) - row.Fields = Fields{ + + row.Fields = model1.Fields{ po.Namespace, - po.ObjectMeta.Name, + po.Name, + computeVulScore(po.ObjectMeta, &po.Spec), "●", strconv.Itoa(cr) + "/" + strconv.Itoa(len(po.Spec.Containers)), phase, - strconv.Itoa(rc), - na(po.Status.PodIP), - na(po.Spec.NodeName), - asNominated(po.Status.NominatedNodeName), - asReadinessGate(po), + strconv.Itoa(rc + irc), toMc(c.cpu), toMi(c.mem), toMc(r.cpu) + ":" + toMc(r.lcpu), @@ -138,10 +152,14 @@ func (p Pod) Render(o interface{}, ns string, row *Row) error { client.ToPercentageStr(c.cpu, r.lcpu), client.ToPercentageStr(c.mem, r.mem), client.ToPercentageStr(c.mem, r.lmem), + na(po.Status.PodIP), + na(po.Spec.NodeName), + asNominated(po.Status.NominatedNodeName), + asReadinessGate(po), p.mapQOS(po.Status.QOSClass), mapToStr(po.Labels), - asStatus(p.diagnose(phase, cr, len(ss))), - toAge(po.GetCreationTimestamp()), + AsStatus(p.diagnose(phase, cr, len(cs))), + ToAge(po.GetCreationTimestamp()), } return nil @@ -205,35 +223,23 @@ func (p *PodWithMetrics) DeepCopyObject() runtime.Object { return p } -func (*Pod) gatherPodMX(pod *v1.Pod, mx *mv1beta1.PodMetrics) (c, r metric) { - rcpu, rmem := podRequests(pod.Spec) - lcpu, lmem := podLimits(pod.Spec) - r.cpu, r.lcpu, r.mem, r.lmem = rcpu.MilliValue(), lcpu.MilliValue(), rmem.Value(), lmem.Value() - if mx != nil { - ccpu, cmem := currentRes(mx) - c.cpu, c.mem = ccpu.MilliValue(), cmem.Value() - } +func gatherCoMX(cc []v1.Container, ccmx []mv1beta1.ContainerMetrics) (c, r metric) { + rcpu, rmem := cosRequests(cc) + r.cpu, r.mem = rcpu.MilliValue(), rmem.Value() - return -} + lcpu, lmem := cosLimits(cc) + r.lcpu, r.lmem = lcpu.MilliValue(), lmem.Value() -func containerRequests(co *v1.Container) v1.ResourceList { - req := co.Resources.Requests - if len(req) != 0 { - return req - } - lim := co.Resources.Limits - if len(lim) != 0 { - return lim - } + ccpu, cmem := currentRes(ccmx) + c.cpu, c.mem = ccpu.MilliValue(), cmem.Value() - return nil + return } -func podLimits(spec v1.PodSpec) (resource.Quantity, resource.Quantity) { +func cosLimits(cc []v1.Container) (resource.Quantity, resource.Quantity) { cpu, mem := new(resource.Quantity), new(resource.Quantity) - for _, co := range spec.Containers { - limits := co.Resources.Limits + for _, c := range cc { + limits := c.Resources.Limits if len(limits) == 0 { return resource.Quantity{}, resource.Quantity{} } @@ -247,10 +253,11 @@ func podLimits(spec v1.PodSpec) (resource.Quantity, resource.Quantity) { return *cpu, *mem } -func podRequests(spec v1.PodSpec) (resource.Quantity, resource.Quantity) { +func cosRequests(cc []v1.Container) (resource.Quantity, resource.Quantity) { cpu, mem := new(resource.Quantity), new(resource.Quantity) - for i := range spec.Containers { - rl := containerRequests(&spec.Containers[i]) + for _, c := range cc { + co := c + rl := containerRequests(&co) if rl.Cpu() != nil { cpu.Add(*rl.Cpu()) } @@ -258,15 +265,16 @@ func podRequests(spec v1.PodSpec) (resource.Quantity, resource.Quantity) { mem.Add(*rl.Memory()) } } + return *cpu, *mem } -func currentRes(mx *mv1beta1.PodMetrics) (resource.Quantity, resource.Quantity) { +func currentRes(ccmx []mv1beta1.ContainerMetrics) (resource.Quantity, resource.Quantity) { cpu, mem := new(resource.Quantity), new(resource.Quantity) - if mx == nil { + if ccmx == nil { return *cpu, *mem } - for _, co := range mx.Containers { + for _, co := range ccmx { c, m := co.Usage.Cpu(), co.Usage.Memory() cpu.Add(*c) mem.Add(*m) @@ -306,13 +314,13 @@ func (*Pod) Statuses(ss []v1.ContainerStatus) (cr, ct, rc int) { func (p *Pod) Phase(po *v1.Pod) string { status := string(po.Status.Phase) if po.Status.Reason != "" { - if po.DeletionTimestamp != nil && po.Status.Reason == "NodeLost" { + if po.DeletionTimestamp != nil && po.Status.Reason == NodeUnreachablePodReason { return "Unknown" } status = po.Status.Reason } - status, ok := p.initContainerPhase(po.Status, len(po.Spec.InitContainers), status) + status, ok := p.initContainerPhase(po, status) if ok { return status } @@ -351,13 +359,16 @@ func (*Pod) containerPhase(st v1.PodStatus, status string) (string, bool) { return status, running } -func (*Pod) initContainerPhase(st v1.PodStatus, initCount int, status string) (string, bool) { - for i, cs := range st.InitContainerStatuses { - s := checkContainerStatus(cs, i, initCount) - if s == "" { - continue +func (*Pod) initContainerPhase(po *v1.Pod, status string) (string, bool) { + count := len(po.Spec.InitContainers) + rs := make(map[string]bool, count) + for _, c := range po.Spec.InitContainers { + rs[c.Name] = restartableInitCO(c.RestartPolicy) + } + for i, cs := range po.Status.InitContainerStatuses { + if s := checkInitContainerStatus(cs, i, count, rs[cs.Name]); s != "" { + return s, true } - return s, true } return status, false @@ -366,7 +377,7 @@ func (*Pod) initContainerPhase(st v1.PodStatus, initCount int, status string) (s // ---------------------------------------------------------------------------- // Helpers.. -func checkContainerStatus(cs v1.ContainerStatus, i, initCount int) string { +func checkInitContainerStatus(cs v1.ContainerStatus, count, initCount int, restartable bool) string { switch { case cs.State.Terminated != nil: if cs.State.Terminated.ExitCode == 0 { @@ -379,11 +390,15 @@ func checkContainerStatus(cs v1.ContainerStatus, i, initCount int) string { return "Init:Signal:" + strconv.Itoa(int(cs.State.Terminated.Signal)) } return "Init:ExitCode:" + strconv.Itoa(int(cs.State.Terminated.ExitCode)) + case restartable && cs.Started != nil && *cs.Started: + if cs.Ready { + return "" + } case cs.State.Waiting != nil && cs.State.Waiting.Reason != "" && cs.State.Waiting.Reason != "PodInitializing": return "Init:" + cs.State.Waiting.Reason - default: - return "Init:" + strconv.Itoa(i) + "/" + strconv.Itoa(initCount) } + + return "Init:" + strconv.Itoa(count) + "/" + strconv.Itoa(initCount) } // PosStatus computes pod status. @@ -406,7 +421,7 @@ func PodStatus(pod *v1.Pod) string { case container.State.Terminated != nil && container.State.Terminated.ExitCode == 0: continue case container.State.Terminated != nil: - if len(container.State.Terminated.Reason) == 0 { + if container.State.Terminated.Reason == "" { if container.State.Terminated.Signal != 0 { reason = fmt.Sprintf("Init:Signal:%d", container.State.Terminated.Signal) } else { @@ -453,7 +468,7 @@ func PodStatus(pod *v1.Pod) string { } } - if pod.DeletionTimestamp != nil && pod.Status.Reason == node.NodeUnreachablePodReason { + if pod.DeletionTimestamp != nil && pod.Status.Reason == NodeUnreachablePodReason { reason = PhaseUnknown } else if pod.DeletionTimestamp != nil { reason = PhaseTerminating @@ -471,3 +486,7 @@ func hasPodReadyCondition(conditions []v1.PodCondition) bool { return false } + +func restartableInitCO(p *v1.ContainerRestartPolicy) bool { + return p != nil && *p == v1.ContainerRestartPolicyAlways +} diff --git a/internal/render/pod_int_test.go b/internal/render/pod_int_test.go new file mode 100644 index 0000000000..74d9bcee2c --- /dev/null +++ b/internal/render/pod_int_test.go @@ -0,0 +1,459 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package render + +import ( + "testing" + + "github.com/derailed/k9s/internal/client" + "github.com/stretchr/testify/assert" + v1 "k8s.io/api/core/v1" + res "k8s.io/apimachinery/pkg/api/resource" + mv1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" +) + +func Test_checkInitContainerStatus(t *testing.T) { + true := true + uu := map[string]struct { + status v1.ContainerStatus + e string + count, total int + restart bool + }{ + "none": { + e: "Init:0/0", + }, + "restart": { + status: v1.ContainerStatus{ + Name: "ic1", + Started: &true, + State: v1.ContainerState{}, + }, + restart: true, + e: "Init:0/0", + }, + "no-restart": { + status: v1.ContainerStatus{ + Name: "ic1", + Started: &true, + State: v1.ContainerState{}, + }, + e: "Init:0/0", + }, + "terminated-reason": { + status: v1.ContainerStatus{ + Name: "ic1", + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + ExitCode: 1, + Reason: "blah", + }, + }, + }, + e: "Init:blah", + }, + "terminated-signal": { + status: v1.ContainerStatus{ + Name: "ic1", + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + ExitCode: 1, + Signal: 9, + }, + }, + }, + e: "Init:Signal:9", + }, + "terminated-code": { + status: v1.ContainerStatus{ + Name: "ic1", + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + ExitCode: 1, + }, + }, + }, + e: "Init:ExitCode:1", + }, + "terminated-restart": { + status: v1.ContainerStatus{ + Name: "ic1", + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "blah", + }, + }, + }, + }, + "waiting": { + status: v1.ContainerStatus{ + Name: "ic1", + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: "blah", + }, + }, + }, + e: "Init:blah", + }, + "waiting-init": { + status: v1.ContainerStatus{ + Name: "ic1", + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: "PodInitializing", + }, + }, + }, + e: "Init:0/0", + }, + "running": { + status: v1.ContainerStatus{ + Name: "ic1", + State: v1.ContainerState{ + Running: &v1.ContainerStateRunning{}, + }, + }, + e: "Init:0/0", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, checkInitContainerStatus(u.status, u.count, u.total, u.restart)) + }) + } +} + +func Test_containerPhase(t *testing.T) { + uu := map[string]struct { + status v1.PodStatus + e string + ok bool + }{ + "none": {}, + "empty": { + status: v1.PodStatus{ + Phase: PhaseUnknown, + }, + }, + "waiting": { + status: v1.PodStatus{ + Phase: PhaseUnknown, + InitContainerStatuses: []v1.ContainerStatus{ + { + Name: "ic1", + State: v1.ContainerState{ + Running: &v1.ContainerStateRunning{}, + }, + }, + }, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: "waiting", + }, + }, + }, + }, + }, + e: "waiting", + }, + "terminated": { + status: v1.PodStatus{ + Phase: PhaseUnknown, + InitContainerStatuses: []v1.ContainerStatus{ + { + Name: "ic1", + State: v1.ContainerState{ + Running: &v1.ContainerStateRunning{}, + }, + }, + }, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "done", + }, + }, + }, + }, + }, + e: "done", + }, + "terminated-sig": { + status: v1.PodStatus{ + Phase: PhaseUnknown, + InitContainerStatuses: []v1.ContainerStatus{ + { + Name: "ic1", + State: v1.ContainerState{ + Running: &v1.ContainerStateRunning{}, + }, + }, + }, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Signal: 9, + }, + }, + }, + }, + }, + e: "Signal:9", + }, + "terminated-code": { + status: v1.PodStatus{ + Phase: PhaseUnknown, + InitContainerStatuses: []v1.ContainerStatus{ + { + Name: "ic1", + State: v1.ContainerState{ + Running: &v1.ContainerStateRunning{}, + }, + }, + }, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + ExitCode: 2, + }, + }, + }, + }, + }, + e: "ExitCode:2", + }, + "running": { + status: v1.PodStatus{ + Phase: PhaseUnknown, + InitContainerStatuses: []v1.ContainerStatus{ + { + Name: "ic1", + State: v1.ContainerState{ + Running: &v1.ContainerStateRunning{}, + }, + }, + }, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + Ready: true, + State: v1.ContainerState{ + Running: &v1.ContainerStateRunning{}, + }, + }, + }, + }, + ok: true, + }, + } + + var p Pod + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + s, ok := p.containerPhase(u.status, "") + assert.Equal(t, u.ok, ok) + assert.Equal(t, u.e, s) + }) + } +} + +func Test_restartableInitCO(t *testing.T) { + always, never := v1.ContainerRestartPolicyAlways, v1.ContainerRestartPolicy("never") + uu := map[string]struct { + p *v1.ContainerRestartPolicy + e bool + }{ + "empty": {}, + "set": { + p: &always, + e: true, + }, + "unset": { + p: &never, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, restartableInitCO(u.p)) + }) + } +} + +func Test_gatherPodMx(t *testing.T) { + uu := map[string]struct { + cc []v1.Container + mx []mv1beta1.ContainerMetrics + c, r metric + perc string + }{ + "single": { + cc: []v1.Container{ + makeContainer("c1", false, "10m", "1Mi", "20m", "2Mi"), + }, + mx: []mv1beta1.ContainerMetrics{ + makeCoMX("c1", "1m", "22Mi"), + }, + c: metric{ + cpu: 1, + mem: 22 * client.MegaByte, + }, + r: metric{ + cpu: 10, + mem: 1 * client.MegaByte, + lcpu: 20, + lmem: 2 * client.MegaByte, + }, + perc: "10", + }, + "multi": { + cc: []v1.Container{ + makeContainer("c1", false, "11m", "22Mi", "111m", "44Mi"), + makeContainer("c2", false, "93m", "1402Mi", "0m", "2804Mi"), + makeContainer("c3", false, "11m", "34Mi", "0m", "69Mi"), + }, + r: metric{ + cpu: 11 + 93 + 11, + mem: (22 + 1402 + 34) * client.MegaByte, + lcpu: 111 + 0 + 0, + lmem: (44 + 2804 + 69) * client.MegaByte, + }, + mx: []mv1beta1.ContainerMetrics{ + makeCoMX("c1", "1m", "22Mi"), + makeCoMX("c2", "51m", "1275Mi"), + makeCoMX("c3", "1m", "27Mi"), + }, + c: metric{ + cpu: 1 + 51 + 1, + mem: (22 + 1275 + 27) * client.MegaByte, + }, + perc: "46", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + c, r := gatherCoMX(u.cc, u.mx) + assert.Equal(t, u.c.cpu, c.cpu) + assert.Equal(t, u.c.mem, c.mem) + assert.Equal(t, u.c.lcpu, c.lcpu) + assert.Equal(t, u.c.lmem, c.lmem) + + assert.Equal(t, u.r.cpu, r.cpu) + assert.Equal(t, u.r.mem, r.mem) + assert.Equal(t, u.r.lcpu, r.lcpu) + assert.Equal(t, u.r.lmem, r.lmem) + + assert.Equal(t, u.perc, client.ToPercentageStr(c.cpu, r.cpu)) + }) + } +} + +func Test_podLimits(t *testing.T) { + uu := map[string]struct { + cc []v1.Container + l v1.ResourceList + }{ + "plain": { + cc: []v1.Container{ + makeContainer("c1", false, "10m", "1Mi", "20m", "2Mi"), + }, + l: makeRes("20m", "2Mi"), + }, + "multi-co": { + cc: []v1.Container{ + makeContainer("c1", false, "10m", "1Mi", "20m", "2Mi"), + makeContainer("c2", false, "10m", "1Mi", "40m", "4Mi"), + }, + l: makeRes("60m", "6Mi"), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + c, m := cosLimits(u.cc) + assert.True(t, c.Equal(*u.l.Cpu())) + assert.True(t, m.Equal(*u.l.Memory())) + }) + } +} + +func Test_podRequests(t *testing.T) { + uu := map[string]struct { + cc []v1.Container + l v1.ResourceList + }{ + "plain": { + cc: []v1.Container{ + makeContainer("c1", false, "10m", "1Mi", "20m", "2Mi"), + }, + l: makeRes("10m", "1Mi"), + }, + "multi-co": { + cc: []v1.Container{ + makeContainer("c1", false, "10m", "1Mi", "20m", "2Mi"), + makeContainer("c2", false, "10m", "1Mi", "40m", "4Mi"), + }, + l: makeRes("20m", "2Mi"), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + c, m := cosRequests(u.cc) + assert.True(t, c.Equal(*u.l.Cpu())) + assert.True(t, m.Equal(*u.l.Memory())) + }) + } +} + +// Helpers... + +func makeContainer(n string, init bool, rc, rm, lc, lm string) v1.Container { + var res v1.ResourceRequirements + if init { + res = v1.ResourceRequirements{} + } else { + res = v1.ResourceRequirements{ + Requests: makeRes(rc, rm), + Limits: makeRes(lc, lm), + } + } + + return v1.Container{Name: n, Resources: res} +} + +func makeRes(c, m string) v1.ResourceList { + cpu, _ := res.ParseQuantity(c) + mem, _ := res.ParseQuantity(m) + + return v1.ResourceList{ + v1.ResourceCPU: cpu, + v1.ResourceMemory: mem, + } +} + +func makeCoMX(n string, c, m string) mv1beta1.ContainerMetrics { + return mv1beta1.ContainerMetrics{ + Name: n, + Usage: makeRes(c, m), + } +} diff --git a/internal/render/pod_test.go b/internal/render/pod_test.go index e439b6caf2..57fe6c4da8 100644 --- a/internal/render/pod_test.go +++ b/internal/render/pod_test.go @@ -1,8 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/derailed/tcell/v2" "github.com/stretchr/testify/assert" @@ -13,128 +17,128 @@ import ( ) func init() { - render.AddColor = tcell.ColorBlue - render.HighlightColor = tcell.ColorYellow - render.CompletedColor = tcell.ColorGray - render.StdColor = tcell.ColorWhite - render.ErrColor = tcell.ColorRed - render.KillColor = tcell.ColorGray + model1.AddColor = tcell.ColorBlue + model1.HighlightColor = tcell.ColorYellow + model1.CompletedColor = tcell.ColorGray + model1.StdColor = tcell.ColorWhite + model1.ErrColor = tcell.ColorRed + model1.KillColor = tcell.ColorGray } func TestPodColorer(t *testing.T) { - stdHeader := render.Header{ - render.HeaderColumn{Name: "NAMESPACE"}, - render.HeaderColumn{Name: "NAME"}, - render.HeaderColumn{Name: "READY"}, - render.HeaderColumn{Name: "RESTARTS"}, - render.HeaderColumn{Name: "STATUS"}, - render.HeaderColumn{Name: "VALID"}, + stdHeader := model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "READY"}, + model1.HeaderColumn{Name: "RESTARTS"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "VALID"}, } uu := map[string]struct { - re render.RowEvent - h render.Header + re model1.RowEvent + h model1.Header e tcell.Color }{ "valid": { h: stdHeader, - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{"blee", "fred", "1/1", "0", render.Running, ""}, + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{"blee", "fred", "1/1", "0", render.Running, ""}, }, }, - e: render.StdColor, + e: model1.StdColor, }, "init": { h: stdHeader, - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{"blee", "fred", "1/1", "0", render.PodInitializing, ""}, + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{"blee", "fred", "1/1", "0", render.PodInitializing, ""}, }, }, - e: render.AddColor, + e: model1.AddColor, }, "init-err": { h: stdHeader, - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{"blee", "fred", "1/1", "0", render.PodInitializing, "blah"}, + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{"blee", "fred", "1/1", "0", render.PodInitializing, "blah"}, }, }, - e: render.AddColor, + e: model1.AddColor, }, "initialized": { h: stdHeader, - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{"blee", "fred", "1/1", "0", render.Initialized, "blah"}, + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{"blee", "fred", "1/1", "0", render.Initialized, "blah"}, }, }, - e: render.HighlightColor, + e: model1.HighlightColor, }, "completed": { h: stdHeader, - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{"blee", "fred", "1/1", "0", render.Completed, "blah"}, + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{"blee", "fred", "1/1", "0", render.Completed, "blah"}, }, }, - e: render.CompletedColor, + e: model1.CompletedColor, }, "terminating": { h: stdHeader, - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{"blee", "fred", "1/1", "0", render.Terminating, "blah"}, + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{"blee", "fred", "1/1", "0", render.Terminating, "blah"}, }, }, - e: render.KillColor, + e: model1.KillColor, }, "invalid": { h: stdHeader, - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{"blee", "fred", "1/1", "0", "Running", "blah"}, + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{"blee", "fred", "1/1", "0", "Running", "blah"}, }, }, - e: render.ErrColor, + e: model1.ErrColor, }, "unknown-cool": { h: stdHeader, - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{"blee", "fred", "1/1", "0", "blee", ""}, + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{"blee", "fred", "1/1", "0", "blee", ""}, }, }, - e: render.AddColor, + e: model1.AddColor, }, "unknown-err": { h: stdHeader, - re: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ - Fields: render.Fields{"blee", "fred", "1/1", "0", "blee", "doh"}, + re: model1.RowEvent{ + Kind: model1.EventAdd, + Row: model1.Row{ + Fields: model1.Fields{"blee", "fred", "1/1", "0", "blee", "doh"}, }, }, - e: render.ErrColor, + e: model1.ErrColor, }, "status": { h: stdHeader[0:3], - re: render.RowEvent{ - Kind: render.EventDelete, - Row: render.Row{ - Fields: render.Fields{"blee", "fred", "1/1", "0", "blee", ""}, + re: model1.RowEvent{ + Kind: model1.EventDelete, + Row: model1.Row{ + Fields: model1.Fields{"blee", "fred", "1/1", "0", "blee", ""}, }, }, - e: render.KillColor, + e: model1.KillColor, }, } @@ -142,7 +146,7 @@ func TestPodColorer(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, r.ColorerFunc()("", u.h, u.re)) + assert.Equal(t, u.e, r.ColorerFunc()("", u.h, &u.re)) }) } } @@ -154,13 +158,13 @@ func TestPodRender(t *testing.T) { } var po render.Pod - r := render.NewRow(14) + r := model1.NewRow(14) err := po.Render(&pom, "", &r) assert.Nil(t, err) assert.Equal(t, "default/nginx", r.ID) - e := render.Fields{"default", "nginx", "●", "1/1", "Running", "0", "172.17.0.6", "minikube", "", "", "100", "50", "100:0", "70:170", "100", "n/a", "71"} - assert.Equal(t, e, r.Fields[:17]) + e := model1.Fields{"default", "nginx", "0", "●", "1/1", "Running", "0", "100", "50", "100:0", "70:170", "100", "n/a", "71", "29", "172.17.0.6", "minikube", "", ""} + assert.Equal(t, e, r.Fields[:19]) } func BenchmarkPodRender(b *testing.B) { @@ -169,7 +173,7 @@ func BenchmarkPodRender(b *testing.B) { MX: makePodMX("nginx", "10m", "10Mi"), } var po render.Pod - r := render.NewRow(12) + r := model1.NewRow(12) b.ReportAllocs() b.ResetTimer() @@ -185,13 +189,13 @@ func TestPodInitRender(t *testing.T) { } var po render.Pod - r := render.NewRow(14) + r := model1.NewRow(14) err := po.Render(&pom, "", &r) assert.Nil(t, err) assert.Equal(t, "default/nginx", r.ID) - e := render.Fields{"default", "nginx", "●", "1/1", "Init:0/1", "0", "172.17.0.6", "minikube", "", "", "10", "10", "100:0", "70:170", "10", "n/a", "14"} - assert.Equal(t, e, r.Fields[:17]) + e := model1.Fields{"default", "nginx", "0", "●", "1/1", "Init:0/1", "0", "10", "10", "100:0", "70:170", "10", "n/a", "14", "5", "172.17.0.6", "minikube", "", ""} + assert.Equal(t, e, r.Fields[:19]) } func TestCheckPodStatus(t *testing.T) { @@ -224,11 +228,31 @@ func TestCheckPodStatus(t *testing.T) { }, e: render.PhaseRunning, }, - "backoff": { + "gated": { pod: v1.Pod{ Status: v1.PodStatus{ + Conditions: []v1.PodCondition{ + {Type: v1.PodScheduled, Reason: v1.PodReasonSchedulingGated}, + }, Phase: v1.PodRunning, InitContainerStatuses: []v1.ContainerStatus{}, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + State: v1.ContainerState{ + Running: &v1.ContainerStateRunning{}, + }, + }, + }, + }, + }, + e: v1.PodReasonSchedulingGated, + }, + + "backoff": { + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: v1.PodRunning, ContainerStatuses: []v1.ContainerStatus{ { Name: "c1", @@ -243,6 +267,256 @@ func TestCheckPodStatus(t *testing.T) { }, e: render.PhaseImagePullBackOff, }, + "backoff-init": { + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: v1.PodRunning, + InitContainerStatuses: []v1.ContainerStatus{ + { + Name: "ic1", + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: render.PhaseImagePullBackOff, + }, + }, + }, + }, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: render.PhaseImagePullBackOff, + }, + }, + }, + }, + }, + }, + e: "Init:ImagePullBackOff", + }, + + "init-terminated-cool": { + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: v1.PodRunning, + InitContainerStatuses: []v1.ContainerStatus{ + { + Name: "ic1", + State: v1.ContainerState{}, + }, + }, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: render.PhaseImagePullBackOff, + }, + }, + }, + }, + }, + }, + e: "Init:0/0", + }, + + "init-terminated-reason": { + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: v1.PodRunning, + InitContainerStatuses: []v1.ContainerStatus{ + { + Name: "ic1", + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + ExitCode: 1, + Reason: "blah", + }, + }, + }, + }, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: render.PhaseImagePullBackOff, + }, + }, + }, + }, + }, + }, + e: "Init:blah", + }, + "init-terminated-sig": { + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: v1.PodRunning, + InitContainerStatuses: []v1.ContainerStatus{ + { + Name: "ic1", + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + ExitCode: 2, + Signal: 9, + }, + }, + }, + }, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: render.PhaseImagePullBackOff, + }, + }, + }, + }, + }, + }, + e: "Init:Signal:9", + }, + "init-terminated-code": { + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: v1.PodRunning, + InitContainerStatuses: []v1.ContainerStatus{ + { + Name: "ic1", + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + ExitCode: 2, + }, + }, + }, + }, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: render.PhaseImagePullBackOff, + }, + }, + }, + }, + }, + }, + e: "Init:ExitCode:2", + }, + + "co-reason": { + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: v1.PodRunning, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + Reason: "blah", + }, + }, + }, + }, + }, + }, + e: "blah", + }, + "co-reason-ready": { + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: v1.PodRunning, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + Ready: true, + State: v1.ContainerState{ + Running: &v1.ContainerStateRunning{}, + }, + }, + }, + }, + }, + e: "Running", + }, + "co-reason-completed": { + pod: v1.Pod{ + Status: v1.PodStatus{ + Conditions: []v1.PodCondition{ + {Type: v1.PodReady, Status: v1.ConditionTrue}, + }, + Phase: render.PhaseCompleted, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + Ready: true, + State: v1.ContainerState{ + Running: &v1.ContainerStateRunning{}, + }, + }, + }, + }, + }, + e: "Running", + }, + + "co-sig": { + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: v1.PodRunning, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + ExitCode: 2, + Signal: 9, + }, + }, + }, + }, + }, + }, + e: "Signal:9", + }, + "co-code": { + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: v1.PodRunning, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + State: v1.ContainerState{ + Terminated: &v1.ContainerStateTerminated{ + ExitCode: 2, + }, + }, + }, + }, + }, + }, + e: "ExitCode:2", + }, + "co-ready": { + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: v1.PodRunning, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + State: v1.ContainerState{ + Running: &v1.ContainerStateRunning{}, + }, + }, + }, + }, + }, + e: "Running", + }, } for k := range uu { @@ -251,7 +525,123 @@ func TestCheckPodStatus(t *testing.T) { assert.Equal(t, u.e, render.PodStatus(&u.pod)) }) } +} + +func TestCheckPhase(t *testing.T) { + always := v1.ContainerRestartPolicyAlways + uu := map[string]struct { + pod v1.Pod + e string + }{ + "unknown": { + pod: v1.Pod{ + Status: v1.PodStatus{ + Phase: render.PhaseUnknown, + }, + }, + e: render.PhaseUnknown, + }, + "terminating": { + pod: v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + DeletionTimestamp: &metav1.Time{Time: testTime()}, + }, + Status: v1.PodStatus{ + Phase: render.PhaseUnknown, + Reason: "bla", + }, + }, + e: render.PhaseTerminating, + }, + "terminating-toast-node": { + pod: v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + DeletionTimestamp: &metav1.Time{Time: testTime()}, + }, + Status: v1.PodStatus{ + Phase: render.PhaseUnknown, + Reason: render.NodeUnreachablePodReason, + }, + }, + e: render.PhaseUnknown, + }, + "restartable": { + pod: v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + DeletionTimestamp: &metav1.Time{Time: testTime()}, + }, + Spec: v1.PodSpec{ + InitContainers: []v1.Container{ + { + Name: "ic1", + RestartPolicy: &always, + }, + }, + }, + Status: v1.PodStatus{ + Phase: render.PhaseUnknown, + Reason: "bla", + InitContainerStatuses: []v1.ContainerStatus{ + { + Name: "ic1", + }, + }, + }, + }, + e: "Init:0/1", + }, + "waiting": { + pod: v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + DeletionTimestamp: &metav1.Time{Time: testTime()}, + }, + Spec: v1.PodSpec{ + InitContainers: []v1.Container{ + { + Name: "ic1", + RestartPolicy: &always, + }, + }, + Containers: []v1.Container{ + { + Name: "c1", + }, + }, + }, + Status: v1.PodStatus{ + Phase: render.PhaseUnknown, + Reason: "bla", + InitContainerStatuses: []v1.ContainerStatus{ + { + Name: "ic1", + State: v1.ContainerState{ + Running: &v1.ContainerStateRunning{}, + }, + }, + }, + ContainerStatuses: []v1.ContainerStatus{ + { + Name: "c1", + State: v1.ContainerState{ + Waiting: &v1.ContainerStateWaiting{ + Reason: "bla", + }, + }, + }, + }, + }, + }, + e: "Init:0/1", + }, + } + var p render.Pod + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, p.Phase(&u.pod)) + }) + } } // ---------------------------------------------------------------------------- @@ -278,123 +668,3 @@ func makeRes(c, m string) v1.ResourceList { v1.ResourceMemory: mem, } } - -// apiVersion: v1 -// kind: Pod -// metadata: -// creationTimestamp: "2023-11-11T17:01:40Z" -// finalizers: -// - batch.kubernetes.io/job-tracking -// generateName: hello-28328646- -// labels: -// batch.kubernetes.io/controller-uid: 35cf5552-7180-48c1-b7b2-8b6e630a7860 -// batch.kubernetes.io/job-name: hello-28328646 -// controller-uid: 35cf5552-7180-48c1-b7b2-8b6e630a7860 -// job-name: hello-28328646 -// name: hello-28328646-h9fnh -// namespace: fred -// ownerReferences: -// - apiVersion: batch/v1 -// blockOwnerDeletion: true -// controller: true -// kind: Job -// name: hello-28328646 -// uid: 35cf5552-7180-48c1-b7b2-8b6e630a7860 -// resourceVersion: "381637" -// uid: ea77c360-6375-459b-8b30-2ac0c59404cd -// spec: -// containers: -// - args: -// - /bin/bash -// - -c -// - for i in {1..5}; do echo "hello";sleep 1; done -// image: blang/busybox-bash -// imagePullPolicy: Always -// name: c1 -// resources: {} -// terminationMessagePath: /dev/termination-log -// terminationMessagePolicy: File -// volumeMounts: -// - mountPath: /var/run/secrets/kubernetes.io/serviceaccount -// name: kube-api-access-7sztm -// readOnly: true -// dnsPolicy: ClusterFirst -// enableServiceLinks: true -// nodeName: kind-worker -// preemptionPolicy: PreemptLowerPriority -// priority: 0 -// restartPolicy: OnFailure -// schedulerName: default-scheduler -// securityContext: {} -// serviceAccount: default -// serviceAccountName: default -// terminationGracePeriodSeconds: 30 -// tolerations: -// - effect: NoExecute -// key: node.kubernetes.io/not-ready -// operator: Exists -// tolerationSeconds: 300 -// - effect: NoExecute -// key: node.kubernetes.io/unreachable -// operator: Exists -// tolerationSeconds: 300 -// volumes: -// - name: kube-api-access-7sztm -// projected: -// defaultMode: 420 -// sources: -// - serviceAccountToken: -// expirationSeconds: 3607 -// path: token -// - configMap: -// items: -// - key: ca.crt -// path: ca.crt -// name: kube-root-ca.crt -// - downwardAPI: -// items: -// - fieldRef: -// apiVersion: v1 -// fieldPath: metadata.namespace -// path: namespace -// status: -// conditions: -// - lastProbeTime: null -// lastTransitionTime: "2023-11-11T17:01:40Z" -// status: "True" -// type: Initialized -// - lastProbeTime: null -// lastTransitionTime: "2023-11-11T17:01:40Z" -// message: 'containers with unready status: [c1[]' -// reason: ContainersNotReady -// status: "False" -// type: Ready -// - lastProbeTime: null -// lastTransitionTime: "2023-11-11T17:01:40Z" -// message: 'containers with unready status: [c1[]' -// reason: ContainersNotReady -// status: "False" -// type: ContainersReady -// - lastProbeTime: null -// lastTransitionTime: "2023-11-11T17:01:40Z" -// status: "True" -// type: PodScheduled -// containerStatuses: -// - image: blang/busybox-bash -// imageID: "" -// lastState: {} -// name: c1 -// ready: false -// restartCount: 0 -// started: false -// state: -// waiting: -// message: Back-off pulling image "blang/busybox-bash" -// reason: ImagePullBackOff -// hostIP: 172.18.0.3 -// phase: Pending -// podIP: 10.244.1.59 -// podIPs: -// - ip: 10.244.1.59 -// qosClass: BestEffort -// startTime: "2023-11-11T17:01:40Z" diff --git a/internal/render/policy.go b/internal/render/policy.go index 9f3ff91941..777ecfaafc 100644 --- a/internal/render/policy.go +++ b/internal/render/policy.go @@ -1,26 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( "fmt" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" "github.com/rs/zerolog/log" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) -func rbacVerbHeader() Header { - return Header{ - HeaderColumn{Name: "GET "}, - HeaderColumn{Name: "LIST "}, - HeaderColumn{Name: "WATCH "}, - HeaderColumn{Name: "CREATE"}, - HeaderColumn{Name: "PATCH "}, - HeaderColumn{Name: "UPDATE"}, - HeaderColumn{Name: "DELETE"}, - HeaderColumn{Name: "DEL-LIST "}, - HeaderColumn{Name: "EXTRAS", Wide: true}, +func rbacVerbHeader() model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "GET "}, + model1.HeaderColumn{Name: "LIST "}, + model1.HeaderColumn{Name: "WATCH "}, + model1.HeaderColumn{Name: "CREATE"}, + model1.HeaderColumn{Name: "PATCH "}, + model1.HeaderColumn{Name: "UPDATE"}, + model1.HeaderColumn{Name: "DELETE"}, + model1.HeaderColumn{Name: "DEL-LIST "}, + model1.HeaderColumn{Name: "EXTRAS", Wide: true}, } } @@ -30,28 +34,28 @@ type Policy struct { } // ColorerFunc colors a resource row. -func (Policy) ColorerFunc() ColorerFunc { - return func(ns string, _ Header, re RowEvent) tcell.Color { +func (Policy) ColorerFunc() model1.ColorerFunc { + return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color { return tcell.ColorMediumSpringGreen } } // Header returns a header row. -func (Policy) Header(ns string) Header { - h := Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "API GROUP"}, - HeaderColumn{Name: "BINDING"}, +func (Policy) Header(ns string) model1.Header { + h := model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "API-GROUP"}, + model1.HeaderColumn{Name: "BINDING"}, } h = append(h, rbacVerbHeader()...) - h = append(h, HeaderColumn{Name: "VALID", Wide: true}) + h = append(h, model1.HeaderColumn{Name: "VALID", Wide: true}) return h } // Render renders a K8s resource to screen. -func (Policy) Render(o interface{}, gvr string, r *Row) error { +func (Policy) Render(o interface{}, gvr string, r *model1.Row) error { p, ok := o.(PolicyRes) if !ok { return fmt.Errorf("expecting PolicyRes but got %T", o) diff --git a/internal/render/policy_test.go b/internal/render/policy_test.go index 3e9943a3d7..b0770d14dc 100644 --- a/internal/render/policy_test.go +++ b/internal/render/policy_test.go @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "errors" "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) @@ -43,7 +47,7 @@ func TestPolicyResMerge(t *testing.T) { func TestPolicyRender(t *testing.T) { var p render.Policy - var r render.Row + var r model1.Row o := render.PolicyRes{ Namespace: "blee", Binding: "fred", @@ -56,7 +60,7 @@ func TestPolicyRender(t *testing.T) { assert.Nil(t, p.Render(o, "fred", &r)) assert.Equal(t, "blee/res", r.ID) - assert.Equal(t, render.Fields{ + assert.Equal(t, model1.Fields{ "blee", "res", "grp", diff --git a/internal/render/popeye.go b/internal/render/popeye.go index b9709008b7..e43df218c6 100644 --- a/internal/render/popeye.go +++ b/internal/render/popeye.go @@ -1,91 +1,84 @@ -package render - -import ( - "fmt" - "math" - "strconv" - "strings" - - "github.com/derailed/k9s/internal/client" - "github.com/derailed/popeye/pkg/config" - "github.com/derailed/tcell/v2" - "github.com/derailed/tview" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" -) - -// Popeye renders a sanitizer to screen. -type Popeye struct { - Base -} - -// ColorerFunc colors a resource row. -func (Popeye) ColorerFunc() ColorerFunc { - return func(ns string, h Header, re RowEvent) tcell.Color { - c := DefaultColorer(ns, h, re) - - warnCol := h.IndexOf("WARNING", true) - status, _ := strconv.Atoi(strings.TrimSpace(re.Row.Fields[warnCol])) - if status > 0 { - c = tcell.ColorOrange - } - errCol := h.IndexOf("ERROR", true) - status, _ = strconv.Atoi(strings.TrimSpace(re.Row.Fields[errCol])) - if status > 0 { - c = ErrColor - } - return c - } -} - -// Header returns a header row. -func (Popeye) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "RESOURCE"}, - HeaderColumn{Name: "SCORE%", Align: tview.AlignRight}, - HeaderColumn{Name: "SCANNED", Align: tview.AlignRight}, - HeaderColumn{Name: "ERROR", Align: tview.AlignRight}, - HeaderColumn{Name: "WARNING", Align: tview.AlignRight}, - HeaderColumn{Name: "INFO", Align: tview.AlignRight}, - HeaderColumn{Name: "OK", Align: tview.AlignRight}, - } -} +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s -// Render renders a K8s resource to screen. -func (Popeye) Render(o interface{}, ns string, r *Row) error { - s, ok := o.(Section) - if !ok { - return fmt.Errorf("expected Section, but got %T", o) - } +package render - r.ID = client.FQN(ns, s.Title) - r.Fields = append(r.Fields, - s.Title, - strconv.Itoa(s.Tally.Score()), - strconv.Itoa(s.Tally.OK+s.Tally.Info+s.Tally.Warning+s.Tally.Error), - strconv.Itoa(s.Tally.Error), - strconv.Itoa(s.Tally.Warning), - strconv.Itoa(s.Tally.Info), - strconv.Itoa(s.Tally.OK), - ) - return nil -} - -// ---------------------------------------------------------------------------- -// Helpers... +import "github.com/derailed/popeye/pkg/config" + +// !!BOZO!! Popeye + +// // Popeye renders a sanitizer to screen. +// type Popeye struct { +// Base +// } + +// // ColorerFunc colors a resource row. +// func (Popeye) ColorerFunc() ColorerFunc { +// return func(ns string, h Header, re *model1.RowEvent) tcell.Color { +// c := DefaultColorer(ns, h, re) + +// warnCol := h.IndexOf("WARNING", true) +// status, _ := strconv.Atoi(strings.TrimSpace(re.Row.Fields[warnCol])) +// if status > 0 { +// c = tcell.ColorOrange +// } +// errCol := h.IndexOf("ERROR", true) +// status, _ = strconv.Atoi(strings.TrimSpace(re.Row.Fields[errCol])) +// if status > 0 { +// c = ErrColor +// } +// return c +// } +// } + +// // Header returns a header row. +// func (Popeye) Header(ns string) model1.Header { +// return model1.Header{ +// model1.HeaderColumn{Name: "RESOURCE"}, +// model1.HeaderColumn{Name: "SCORE%", Align: tview.AlignRight}, +// model1.HeaderColumn{Name: "SCANNED", Align: tview.AlignRight}, +// model1.HeaderColumn{Name: "ERROR", Align: tview.AlignRight}, +// model1.HeaderColumn{Name: "WARNING", Align: tview.AlignRight}, +// model1.HeaderColumn{Name: "INFO", Align: tview.AlignRight}, +// model1.HeaderColumn{Name: "OK", Align: tview.AlignRight}, +// } +// } + +// // Render renders a K8s resource to screen. +// func (Popeye) Render(o interface{}, ns string, r *model1.Row) error { +// s, ok := o.(Section) +// if !ok { +// return fmt.Errorf("expected Section, but got %T", o) +// } + +// r.ID = client.FQN(ns, s.Title) +// r.Fields = append(r.Fields, +// s.Title, +// strconv.Itoa(s.Tally.Score()), +// strconv.Itoa(s.Tally.OK+s.Tally.Info+s.Tally.Warning+s.Tally.Error), +// strconv.Itoa(s.Tally.Error), +// strconv.Itoa(s.Tally.Warning), +// strconv.Itoa(s.Tally.Info), +// strconv.Itoa(s.Tally.OK), +// ) +// return nil +// } + +// // ---------------------------------------------------------------------------- +// // Helpers... type ( - // Builder represents a popeye report. - Builder struct { - Report Report `json:"popeye" yaml:"popeye"` - } - - // Report represents the output of a sanitization pass. - Report struct { - Score int `json:"score" yaml:"score"` - Grade string `json:"grade" yaml:"grade"` - Sections Sections `json:"sanitizers,omitempty" yaml:"sanitizers,omitempty"` - } + // // Builder represents a popeye report. + // Builder struct { + // Report Report `json:"popeye" yaml:"popeye"` + // } + + // // Report represents the output of a sanitization pass. + // Report struct { + // Score int `json:"score" yaml:"score"` + // Grade string `json:"grade" yaml:"grade"` + // Sections Sections `json:"sanitizers,omitempty" yaml:"sanitizers,omitempty"` + // } // Sections represents a collection of sections. Sections []Section @@ -113,89 +106,90 @@ type ( } // Tally tracks a section scores. + Tally struct { OK, Info, Warning, Error int Count int } ) -// Sum sums up tally counts. -func (t *Tally) Sum() int { - return t.OK + t.Info + t.Warning + t.Error -} - -// Score returns the overall sections score in percent. -func (t *Tally) Score() int { - oks := t.OK + t.Info - return toPerc(float64(oks), float64(oks+t.Warning+t.Error)) -} - -func toPerc(v1, v2 float64) int { - if v2 == 0 { - return 0 - } - return int(math.Floor((v1 / v2) * 100)) -} - -// Len returns a section length. -func (s Sections) Len() int { - return len(s) -} - -// Swap swaps values. -func (s Sections) Swap(i, j int) { - s[i], s[j] = s[j], s[i] -} - -// Less compares section scores. -func (s Sections) Less(i, j int) bool { - t1, t2 := s[i].Tally, s[j].Tally - return t1.Score() < t2.Score() -} - -// GetObjectKind returns a schema object. -func (Section) GetObjectKind() schema.ObjectKind { - return nil -} - -// DeepCopyObject returns a container copy. -func (s Section) DeepCopyObject() runtime.Object { - return s -} - -// MaxSeverity gather the max severity in a collection of issues. -func (s Section) MaxSeverity() config.Level { - max := config.OkLevel - for _, issues := range s.Outcome { - m := issues.MaxSeverity() - if m > max { - max = m - } - } - - return max -} - -// MaxSeverity gather the max severity in a collection of issues. -func (i Issues) MaxSeverity() config.Level { - max := config.OkLevel - for _, is := range i { - if is.Level > max { - max = is.Level - } - } - - return max -} - -// CountSeverity counts severity level instances. -func (i Issues) CountSeverity(l config.Level) int { - var count int - for _, is := range i { - if is.Level == l { - count++ - } - } - - return count -} +// // Sum sums up tally counts. +// func (t *Tally) Sum() int { +// return t.OK + t.Info + t.Warning + t.Error +// } + +// // Score returns the overall sections score in percent. +// func (t *Tally) Score() int { +// oks := t.OK + t.Info +// return toPerc(float64(oks), float64(oks+t.Warning+t.Error)) +// } + +// func toPerc(v1, v2 float64) int { +// if v2 == 0 { +// return 0 +// } +// return int(math.Floor((v1 / v2) * 100)) +// } + +// // Len returns a section length. +// func (s Sections) Len() int { +// return len(s) +// } + +// // Swap swaps values. +// func (s Sections) Swap(i, j int) { +// s[i], s[j] = s[j], s[i] +// } + +// // Less compares section scores. +// func (s Sections) Less(i, j int) bool { +// t1, t2 := s[i].Tally, s[j].Tally +// return t1.Score() < t2.Score() +// } + +// // GetObjectKind returns a schema object. +// func (Section) GetObjectKind() schema.ObjectKind { +// return nil +// } + +// // DeepCopyObject returns a container copy. +// func (s Section) DeepCopyObject() runtime.Object { +// return s +// } + +// // MaxSeverity gather the max severity in a collection of issues. +// func (s Section) MaxSeverity() config.Level { +// max := config.OkLevel +// for _, issues := range s.Outcome { +// m := issues.MaxSeverity() +// if m > max { +// max = m +// } +// } + +// return max +// } + +// // MaxSeverity gather the max severity in a collection of issues. +// func (i Issues) MaxSeverity() config.Level { +// max := config.OkLevel +// for _, is := range i { +// if is.Level > max { +// max = is.Level +// } +// } + +// return max +// } + +// // CountSeverity counts severity level instances. +// func (i Issues) CountSeverity(l config.Level) int { +// var count int +// for _, is := range i { +// if is.Level == l { +// count++ +// } +// } + +// return count +// } diff --git a/internal/render/port_forward_test.go b/internal/render/port_forward_test.go index d8728f8fb3..6c4cd18819 100644 --- a/internal/render/port_forward_test.go +++ b/internal/render/port_forward_test.go @@ -1,15 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "time" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestPortForwardRender(t *testing.T) { - var p render.PortForward - var r render.Row o := render.ForwardRes{ Forwarder: fwd{}, Config: render.BenchCfg{ @@ -20,9 +23,11 @@ func TestPortForwardRender(t *testing.T) { }, } + var p render.PortForward + var r model1.Row assert.Nil(t, p.Render(o, "fred", &r)) assert.Equal(t, "blee/fred", r.ID) - assert.Equal(t, render.Fields{ + assert.Equal(t, model1.Fields{ "blee", "fred", "co", @@ -31,8 +36,7 @@ func TestPortForwardRender(t *testing.T) { "1", "1", "", - "2m", - }, r.Fields) + }, r.Fields[:8]) } // Helpers... @@ -59,6 +63,6 @@ func (f fwd) Active() bool { return true } -func (f fwd) Age() string { - return "2m" +func (f fwd) Age() time.Time { + return testTime() } diff --git a/internal/render/portforward.go b/internal/render/portforward.go index 2a039acf93..267be33cf6 100644 --- a/internal/render/portforward.go +++ b/internal/render/portforward.go @@ -1,11 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( "fmt" "strings" + "time" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -25,7 +31,7 @@ type Forwarder interface { Active() bool // Age returns forwarder age. - Age() string + Age() time.Time } // PortForward renders a portforwards to screen. @@ -34,29 +40,29 @@ type PortForward struct { } // ColorerFunc colors a resource row. -func (PortForward) ColorerFunc() ColorerFunc { - return func(ns string, _ Header, re RowEvent) tcell.Color { +func (PortForward) ColorerFunc() model1.ColorerFunc { + return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color { return tcell.ColorSkyblue } } // Header returns a header row. -func (PortForward) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "CONTAINER"}, - HeaderColumn{Name: "PORTS"}, - HeaderColumn{Name: "URL"}, - HeaderColumn{Name: "C"}, - HeaderColumn{Name: "N"}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (PortForward) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "CONTAINER"}, + model1.HeaderColumn{Name: "PORTS"}, + model1.HeaderColumn{Name: "URL"}, + model1.HeaderColumn{Name: "C"}, + model1.HeaderColumn{Name: "N"}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (f PortForward) Render(o interface{}, gvr string, r *Row) error { +func (f PortForward) Render(o interface{}, gvr string, r *model1.Row) error { pf, ok := o.(ForwardRes) if !ok { return fmt.Errorf("expecting a ForwardRes but got %T", o) @@ -66,7 +72,7 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error { r.ID = pf.ID() ns, n := client.Namespaced(r.ID) - r.Fields = Fields{ + r.Fields = model1.Fields{ ns, trimContainer(n), pf.Container(), @@ -75,7 +81,7 @@ func (f PortForward) Render(o interface{}, gvr string, r *Row) error { AsThousands(int64(pf.Config.C)), AsThousands(int64(pf.Config.N)), "", - pf.Age(), + ToAge(metav1.Time{Time: pf.Age()}), } return nil diff --git a/internal/render/pv.go b/internal/render/pv.go index 630c2e77ac..9e42893725 100644 --- a/internal/render/pv.go +++ b/internal/render/pv.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -6,6 +9,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -20,54 +24,52 @@ type PersistentVolume struct { } // ColorerFunc colors a resource row. -func (p PersistentVolume) ColorerFunc() ColorerFunc { - return func(ns string, h Header, re RowEvent) tcell.Color { - if !Happy(ns, h, re.Row) { - return ErrColor - } +func (p PersistentVolume) ColorerFunc() model1.ColorerFunc { + return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { + c := model1.DefaultColorer(ns, h, re) - statusCol := h.IndexOf("STATUS", true) - if statusCol == -1 { - return DefaultColorer(ns, h, re) + idx, ok := h.IndexOf("STATUS", true) + if ok { + return c } - switch strings.TrimSpace(re.Row.Fields[statusCol]) { + switch strings.TrimSpace(re.Row.Fields[idx]) { case string(v1.VolumeBound): - return StdColor + return model1.StdColor case string(v1.VolumeAvailable): return tcell.ColorGreen case string(v1.VolumePending): - return PendingColor + return model1.PendingColor case terminatingPhase: - return CompletedColor + return model1.CompletedColor } - return DefaultColorer(ns, h, re) + return c } } // Header returns a header rbw. -func (PersistentVolume) Header(string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "CAPACITY", Capacity: true}, - HeaderColumn{Name: "ACCESS MODES"}, - HeaderColumn{Name: "RECLAIM POLICY"}, - HeaderColumn{Name: "STATUS"}, - HeaderColumn{Name: "CLAIM"}, - HeaderColumn{Name: "STORAGECLASS"}, - HeaderColumn{Name: "REASON"}, - HeaderColumn{Name: "VOLUMEMODE", Wide: true}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (PersistentVolume) Header(string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "CAPACITY", Capacity: true}, + model1.HeaderColumn{Name: "ACCESS MODES"}, + model1.HeaderColumn{Name: "RECLAIM POLICY"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "CLAIM"}, + model1.HeaderColumn{Name: "STORAGECLASS"}, + model1.HeaderColumn{Name: "REASON"}, + model1.HeaderColumn{Name: "VOLUMEMODE", Wide: true}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (p PersistentVolume) Render(o interface{}, ns string, r *Row) error { +func (p PersistentVolume) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected PersistentVolume, but got %T", o) + return fmt.Errorf("expected PersistentVolume, but got %T", o) } var pv v1.PersistentVolume err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &pv) @@ -91,7 +93,7 @@ func (p PersistentVolume) Render(o interface{}, ns string, r *Row) error { size := pv.Spec.Capacity[v1.ResourceStorage] r.ID = client.MetaFQN(pv.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ pv.Name, size.String(), accessMode(pv.Spec.AccessModes), @@ -102,8 +104,8 @@ func (p PersistentVolume) Render(o interface{}, ns string, r *Row) error { pv.Status.Reason, p.volumeMode(pv.Spec.VolumeMode), mapToStr(pv.Labels), - asStatus(p.diagnose(phase)), - toAge(pv.GetCreationTimestamp()), + AsStatus(p.diagnose(phase)), + ToAge(pv.GetCreationTimestamp()), } return nil diff --git a/internal/render/pv_test.go b/internal/render/pv_test.go index d1e4cf21ff..615fd8b6c5 100644 --- a/internal/render/pv_test.go +++ b/internal/render/pv_test.go @@ -1,26 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestPersistentVolumeRender(t *testing.T) { c := render.PersistentVolume{} - r := render.NewRow(9) + r := model1.NewRow(9) assert.NoError(t, c.Render(load(t, "pv"), "-", &r)) assert.Equal(t, "-/pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b", r.ID) - assert.Equal(t, render.Fields{"pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b", "1Gi", "RWO", "Delete", "Bound", "default/www-nginx-sts-1", "standard"}, r.Fields[:7]) + assert.Equal(t, model1.Fields{"pvc-07aa4e2c-8726-11e9-a8e8-42010a80015b", "1Gi", "RWO", "Delete", "Bound", "default/www-nginx-sts-1", "standard"}, r.Fields[:7]) } func TestTerminatingPersistentVolumeRender(t *testing.T) { c := render.PersistentVolume{} - r := render.NewRow(9) + r := model1.NewRow(9) assert.NoError(t, c.Render(load(t, "pv_terminating"), "-", &r)) assert.Equal(t, "-/pvc-a4d86f51-916c-476b-83af-b551c91a8ac0", r.ID) - assert.Equal(t, render.Fields{"pvc-a4d86f51-916c-476b-83af-b551c91a8ac0", "1Gi", "RWO", "Delete", "Terminating", "default/www-nginx-sts-2", "standard"}, r.Fields[:7]) + assert.Equal(t, model1.Fields{"pvc-a4d86f51-916c-476b-83af-b551c91a8ac0", "1Gi", "RWO", "Delete", "Terminating", "default/www-nginx-sts-2", "standard"}, r.Fields[:7]) } diff --git a/internal/render/pvc.go b/internal/render/pvc.go index f5514d5b58..79678346bc 100644 --- a/internal/render/pvc.go +++ b/internal/render/pvc.go @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( "fmt" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -15,26 +19,26 @@ type PersistentVolumeClaim struct { } // Header returns a header rbw. -func (PersistentVolumeClaim) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "STATUS"}, - HeaderColumn{Name: "VOLUME"}, - HeaderColumn{Name: "CAPACITY", Capacity: true}, - HeaderColumn{Name: "ACCESS MODES"}, - HeaderColumn{Name: "STORAGECLASS"}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (PersistentVolumeClaim) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "VOLUME"}, + model1.HeaderColumn{Name: "CAPACITY", Capacity: true}, + model1.HeaderColumn{Name: "ACCESS MODES"}, + model1.HeaderColumn{Name: "STORAGECLASS"}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *Row) error { +func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected PersistentVolumeClaim, but got %T", o) + return fmt.Errorf("expected PersistentVolumeClaim, but got %T", o) } var pvc v1.PersistentVolumeClaim err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &pvc) @@ -62,7 +66,7 @@ func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *Row) error { } r.ID = client.MetaFQN(pvc.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ pvc.Namespace, pvc.Name, string(phase), @@ -71,8 +75,8 @@ func (p PersistentVolumeClaim) Render(o interface{}, ns string, r *Row) error { accessModes, class, mapToStr(pvc.Labels), - asStatus(p.diagnose(string(phase))), - toAge(pvc.GetCreationTimestamp()), + AsStatus(p.diagnose(string(phase))), + ToAge(pvc.GetCreationTimestamp()), } return nil diff --git a/internal/render/pvc_test.go b/internal/render/pvc_test.go index 8a210efd15..ec85c2e180 100644 --- a/internal/render/pvc_test.go +++ b/internal/render/pvc_test.go @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestPersistentVolumeClaimRender(t *testing.T) { c := render.PersistentVolumeClaim{} - r := render.NewRow(8) + r := model1.NewRow(8) assert.NoError(t, c.Render(load(t, "pvc"), "", &r)) assert.Equal(t, "default/www-nginx-sts-0", r.ID) - assert.Equal(t, render.Fields{"default", "www-nginx-sts-0", "Bound", "pvc-fbabd470-8725-11e9-a8e8-42010a80015b", "1Gi", "RWO", "standard"}, r.Fields[:7]) + assert.Equal(t, model1.Fields{"default", "www-nginx-sts-0", "Bound", "pvc-fbabd470-8725-11e9-a8e8-42010a80015b", "1Gi", "RWO", "standard"}, r.Fields[:7]) } diff --git a/internal/render/rbac.go b/internal/render/rbac.go index 5d0f7a3cd7..12ad96e7a5 100644 --- a/internal/render/rbac.go +++ b/internal/render/rbac.go @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( "fmt" "strings" + "github.com/derailed/k9s/internal/model1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -34,31 +38,31 @@ type Rbac struct { } // ColorerFunc colors a resource row. -func (Rbac) ColorerFunc() ColorerFunc { - return DefaultColorer +func (Rbac) ColorerFunc() model1.ColorerFunc { + return model1.DefaultColorer } // Header returns a header row. -func (Rbac) Header(ns string) Header { - h := make(Header, 0, 10) +func (Rbac) Header(ns string) model1.Header { + h := make(model1.Header, 0, 10) h = append(h, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "APIGROUP"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "API-GROUP"}, ) h = append(h, rbacVerbHeader()...) - return append(h, HeaderColumn{Name: "VALID", Wide: true}) + return append(h, model1.HeaderColumn{Name: "VALID", Wide: true}) } // Render renders a K8s resource to screen. -func (r Rbac) Render(o interface{}, ns string, ro *Row) error { +func (r Rbac) Render(o interface{}, ns string, ro *model1.Row) error { p, ok := o.(PolicyRes) if !ok { return fmt.Errorf("expecting RuleRes but got %T", o) } ro.ID = p.Resource - ro.Fields = make(Fields, 0, len(r.Header(ns))) + ro.Fields = make(model1.Fields, 0, len(r.Header(ns))) ro.Fields = append(ro.Fields, cleanseResource(p.Resource), p.Group, diff --git a/internal/render/reference.go b/internal/render/reference.go index 33b8493c09..21dec9d75d 100644 --- a/internal/render/reference.go +++ b/internal/render/reference.go @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( "fmt" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -15,24 +19,24 @@ type Reference struct { } // ColorerFunc colors a resource row. -func (Reference) ColorerFunc() ColorerFunc { - return func(ns string, _ Header, re RowEvent) tcell.Color { +func (Reference) ColorerFunc() model1.ColorerFunc { + return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color { return tcell.ColorCadetBlue } } // Header returns a header row. -func (Reference) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "GVR"}, +func (Reference) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "GVR"}, } } // Render renders a K8s resource to screen. // BOZO!! Pass in a row with pre-alloc fields?? -func (Reference) Render(o interface{}, ns string, r *Row) error { +func (Reference) Render(o interface{}, ns string, r *model1.Row) error { ref, ok := o.(ReferenceRes) if !ok { return fmt.Errorf("expected ReferenceRes, but got %T", o) diff --git a/internal/render/reference_test.go b/internal/render/reference_test.go index 697ffcc19c..46aaf70094 100644 --- a/internal/render/reference_test.go +++ b/internal/render/reference_test.go @@ -1,8 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) @@ -16,11 +20,11 @@ func TestReferenceRender(t *testing.T) { var ( ref = render.Reference{} - r render.Row + r model1.Row ) assert.Nil(t, ref.Render(o, "fred", &r)) assert.Equal(t, "ns1/blee", r.ID) - assert.Equal(t, render.Fields{ + assert.Equal(t, model1.Fields{ "ns1", "blee", "v1/secrets", diff --git a/internal/render/render_test.go b/internal/render/render_test.go index 20a1c331bc..13192cb9ab 100644 --- a/internal/render/render_test.go +++ b/internal/render/render_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( diff --git a/internal/render/ro.go b/internal/render/ro.go index c1ce340fa6..7b3ce31549 100644 --- a/internal/render/ro.go +++ b/internal/render/ro.go @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( "fmt" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -15,25 +19,25 @@ type Role struct { } // Header returns a header row. -func (Role) Header(ns string) Header { - var h Header +func (Role) Header(ns string) model1.Header { + var h model1.Header if client.IsAllNamespaces(ns) { - h = append(h, HeaderColumn{Name: "NAMESPACE"}) + h = append(h, model1.HeaderColumn{Name: "NAMESPACE"}) } return append(h, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, ) } // Render renders a K8s resource to screen. -func (r Role) Render(o interface{}, ns string, row *Row) error { +func (r Role) Render(o interface{}, ns string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected Role, but got %T", o) + return fmt.Errorf("expected Role, but got %T", o) } var ro rbacv1.Role err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ro) @@ -42,7 +46,7 @@ func (r Role) Render(o interface{}, ns string, row *Row) error { } row.ID = client.MetaFQN(ro.ObjectMeta) - row.Fields = make(Fields, 0, len(r.Header(ns))) + row.Fields = make(model1.Fields, 0, len(r.Header(ns))) if client.IsAllNamespaces(ns) { row.Fields = append(row.Fields, ro.Namespace) } @@ -50,7 +54,7 @@ func (r Role) Render(o interface{}, ns string, row *Row) error { ro.Name, mapToStr(ro.Labels), "", - toAge(ro.GetCreationTimestamp()), + ToAge(ro.GetCreationTimestamp()), ) return nil diff --git a/internal/render/ro_test.go b/internal/render/ro_test.go index b2bbe66c89..5beb907d4d 100644 --- a/internal/render/ro_test.go +++ b/internal/render/ro_test.go @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestRoleRender(t *testing.T) { c := render.Role{} - r := render.NewRow(3) + r := model1.NewRow(3) assert.NoError(t, c.Render(load(t, "ro"), "", &r)) assert.Equal(t, "default/blee", r.ID) - assert.Equal(t, render.Fields{"default", "blee"}, r.Fields[:2]) + assert.Equal(t, model1.Fields{"default", "blee"}, r.Fields[:2]) } diff --git a/internal/render/rob.go b/internal/render/rob.go index 9c9f6f8d77..1f58fd609c 100644 --- a/internal/render/rob.go +++ b/internal/render/rob.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -5,6 +8,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -16,28 +20,28 @@ type RoleBinding struct { } // Header returns a header rbw. -func (RoleBinding) Header(ns string) Header { - var h Header +func (RoleBinding) Header(ns string) model1.Header { + var h model1.Header if client.IsAllNamespaces(ns) { - h = append(h, HeaderColumn{Name: "NAMESPACE"}) + h = append(h, model1.HeaderColumn{Name: "NAMESPACE"}) } return append(h, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "ROLE"}, - HeaderColumn{Name: "KIND"}, - HeaderColumn{Name: "SUBJECTS"}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "ROLE"}, + model1.HeaderColumn{Name: "KIND"}, + model1.HeaderColumn{Name: "SUBJECTS"}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, ) } // Render renders a K8s resource to screen. -func (r RoleBinding) Render(o interface{}, ns string, row *Row) error { +func (r RoleBinding) Render(o interface{}, ns string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected RoleBinding, but got %T", o) + return fmt.Errorf("expected RoleBinding, but got %T", o) } var rb rbacv1.RoleBinding err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &rb) @@ -48,7 +52,7 @@ func (r RoleBinding) Render(o interface{}, ns string, row *Row) error { kind, ss := renderSubjects(rb.Subjects) row.ID = client.MetaFQN(rb.ObjectMeta) - row.Fields = make(Fields, 0, len(r.Header(ns))) + row.Fields = make(model1.Fields, 0, len(r.Header(ns))) if client.IsAllNamespaces(ns) { row.Fields = append(row.Fields, rb.Namespace) } @@ -59,7 +63,7 @@ func (r RoleBinding) Render(o interface{}, ns string, row *Row) error { ss, mapToStr(rb.Labels), "", - toAge(rb.GetCreationTimestamp()), + ToAge(rb.GetCreationTimestamp()), ) return nil diff --git a/internal/render/rob_test.go b/internal/render/rob_test.go index cb3e488261..f18a08bf26 100644 --- a/internal/render/rob_test.go +++ b/internal/render/rob_test.go @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestRoleBindingRender(t *testing.T) { c := render.RoleBinding{} - r := render.NewRow(6) + r := model1.NewRow(6) assert.NoError(t, c.Render(load(t, "rb"), "", &r)) assert.Equal(t, "default/blee", r.ID) - assert.Equal(t, render.Fields{"default", "blee", "blee", "SvcAcct", "fernand"}, r.Fields[:5]) + assert.Equal(t, model1.Fields{"default", "blee", "blee", "SvcAcct", "fernand"}, r.Fields[:5]) } diff --git a/internal/render/row.go b/internal/render/row.go deleted file mode 100644 index 1bd5e3872f..0000000000 --- a/internal/render/row.go +++ /dev/null @@ -1,214 +0,0 @@ -package render - -import ( - "reflect" - "sort" - "strings" - - "github.com/fvbommel/sortorder" -) - -// Fields represents a collection of row fields. -type Fields []string - -// Customize returns a subset of fields. -func (f Fields) Customize(cols []int, out Fields) { - for i, c := range cols { - if c < 0 { - out[i] = NAValue - continue - } - if c < len(f) { - out[i] = f[c] - } - } -} - -// Diff returns true if fields differ or false otherwise. -func (f Fields) Diff(ff Fields, ageCol int) bool { - if ageCol < 0 { - return !reflect.DeepEqual(f[:len(f)-1], ff[:len(ff)-1]) - } - if !reflect.DeepEqual(f[:ageCol], ff[:ageCol]) { - return true - } - return !reflect.DeepEqual(f[ageCol+1:], ff[ageCol+1:]) -} - -// Clone returns a copy of the fields. -func (f Fields) Clone() Fields { - cp := make(Fields, len(f)) - copy(cp, f) - - return cp -} - -// ---------------------------------------------------------------------------- - -// Row represents a collection of columns. -type Row struct { - ID string - Fields Fields -} - -// NewRow returns a new row with initialized fields. -func NewRow(size int) Row { - return Row{Fields: make([]string, size)} -} - -// Labelize returns a new row based on labels. -func (r Row) Labelize(cols []int, labelCol int, labels []string) Row { - out := NewRow(len(cols) + len(labels)) - for _, col := range cols { - out.Fields = append(out.Fields, r.Fields[col]) - } - m := labelize(r.Fields[labelCol]) - for _, label := range labels { - out.Fields = append(out.Fields, m[label]) - } - - return out -} - -// Customize returns a row subset based on given col indices. -func (r Row) Customize(cols []int) Row { - out := NewRow(len(cols)) - r.Fields.Customize(cols, out.Fields) - out.ID = r.ID - - return out -} - -// Diff returns true if row differ or false otherwise. -func (r Row) Diff(ro Row, ageCol int) bool { - if r.ID != ro.ID { - return true - } - return r.Fields.Diff(ro.Fields, ageCol) -} - -// Clone copies a row. -func (r Row) Clone() Row { - return Row{ - ID: r.ID, - Fields: r.Fields.Clone(), - } -} - -// Len returns the length of the row. -func (r Row) Len() int { - return len(r.Fields) -} - -// ---------------------------------------------------------------------------- - -// Rows represents a collection of rows. -type Rows []Row - -// Delete removes an element by id. -func (rr Rows) Delete(id string) Rows { - idx, ok := rr.Find(id) - if !ok { - return rr - } - - if idx == 0 { - return rr[1:] - } - if idx+1 == len(rr) { - return rr[:len(rr)-1] - } - - return append(rr[:idx], rr[idx+1:]...) -} - -// Upsert adds a new item. -func (rr Rows) Upsert(r Row) Rows { - idx, ok := rr.Find(r.ID) - if !ok { - return append(rr, r) - } - rr[idx] = r - - return rr -} - -// Find locates a row by id. Returns false is not found. -func (rr Rows) Find(id string) (int, bool) { - for i, r := range rr { - if r.ID == id { - return i, true - } - } - - return 0, false -} - -// Sort rows based on column index and order. -func (rr Rows) Sort(col int, asc, isNum, isDur, isCapacity bool) { - t := RowSorter{ - Rows: rr, - Index: col, - IsNumber: isNum, - IsDuration: isDur, - IsCapacity: isCapacity, - Asc: asc, - } - sort.Sort(t) -} - -// ---------------------------------------------------------------------------- - -// RowSorter sorts rows. -type RowSorter struct { - Rows Rows - Index int - IsNumber bool - IsDuration bool - IsCapacity bool - Asc bool -} - -func (s RowSorter) Len() int { - return len(s.Rows) -} - -func (s RowSorter) Swap(i, j int) { - s.Rows[i], s.Rows[j] = s.Rows[j], s.Rows[i] -} - -func (s RowSorter) Less(i, j int) bool { - v1, v2 := s.Rows[i].Fields[s.Index], s.Rows[j].Fields[s.Index] - id1, id2 := s.Rows[i].ID, s.Rows[j].ID - less := Less(s.IsNumber, s.IsDuration, s.IsCapacity, id1, id2, v1, v2) - if s.Asc { - return less - } - return !less -} - -// ---------------------------------------------------------------------------- -// Helpers... - -// Less return true if c1 < c2. -func Less(isNumber, isDuration, isCapacity bool, id1, id2, v1, v2 string) bool { - var less bool - switch { - case isNumber: - v1, v2 = strings.Replace(v1, ",", "", -1), strings.Replace(v2, ",", "", -1) - less = sortorder.NaturalLess(v1, v2) - case isDuration: - d1, d2 := durationToSeconds(v1), durationToSeconds(v2) - less = d1 <= d2 - case isCapacity: - c1, c2 := capacityToNumber(v1), capacityToNumber(v2) - less = c1 <= c2 - default: - less = sortorder.NaturalLess(v1, v2) - } - if v1 == v2 { - return sortorder.NaturalLess(id1, id2) - } - - return less -} diff --git a/internal/render/row_event_test.go b/internal/render/row_event_test.go deleted file mode 100644 index c106673098..0000000000 --- a/internal/render/row_event_test.go +++ /dev/null @@ -1,536 +0,0 @@ -package render_test - -import ( - "testing" - "time" - - "github.com/derailed/k9s/internal/render" - "github.com/stretchr/testify/assert" -) - -func TestRowEventCustomize(t *testing.T) { - uu := map[string]struct { - re1, e render.RowEvent - cols []int - }{ - "empty": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - e: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{}}, - }, - }, - "full": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - e: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - cols: []int{0, 1, 2}, - }, - "deltas": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - Deltas: render.DeltaRow{"a", "b", "c"}, - }, - e: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - Deltas: render.DeltaRow{"a", "b", "c"}, - }, - cols: []int{0, 1, 2}, - }, - "deltas-skip": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - Deltas: render.DeltaRow{"a", "b", "c"}, - }, - e: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"3", "1"}}, - Deltas: render.DeltaRow{"c", "a"}, - }, - cols: []int{2, 0}, - }, - "reverse": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - e: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"3", "2", "1"}}, - }, - cols: []int{2, 1, 0}, - }, - "skip": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - e: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"3", "1"}}, - }, - cols: []int{2, 0}, - }, - "miss": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - e: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"3", "", "1"}}, - }, - cols: []int{2, 10, 0}, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.re1.Customize(u.cols)) - }) - } -} - -func TestRowEventDiff(t *testing.T) { - uu := map[string]struct { - re1, re2 render.RowEvent - e bool - }{ - "same": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - re2: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - }, - "diff-kind": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - re2: render.RowEvent{ - Kind: render.EventDelete, - Row: render.Row{ID: "B", Fields: render.Fields{"1", "2", "3"}}, - }, - e: true, - }, - "diff-delta": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - Deltas: render.DeltaRow{"1", "2", "3"}, - }, - re2: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - Deltas: render.DeltaRow{"10", "2", "3"}, - }, - e: true, - }, - "diff-id": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - re2: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "B", Fields: render.Fields{"1", "2", "3"}}, - }, - e: true, - }, - "diff-field": { - re1: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - }, - re2: render.RowEvent{ - Kind: render.EventAdd, - Row: render.Row{ID: "A", Fields: render.Fields{"10", "2", "3"}}, - }, - e: true, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.re1.Diff(u.re2, -1)) - }) - } -} - -func TestRowEventsDiff(t *testing.T) { - uu := map[string]struct { - re1, re2 render.RowEvents - ageCol int - e bool - }{ - "same": { - re1: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - re2: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - ageCol: -1, - }, - "diff-len": { - re1: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - re2: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - ageCol: -1, - e: true, - }, - "diff-id": { - re1: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - re2: render.RowEvents{ - {Row: render.Row{ID: "D", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - ageCol: -1, - e: true, - }, - "diff-order": { - re1: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - re2: render.RowEvents{ - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - ageCol: -1, - e: true, - }, - "diff-withAge": { - re1: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - re2: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "13"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - ageCol: 1, - e: true, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.re1.Diff(u.re2, u.ageCol)) - }) - } -} - -func TestRowEventsUpsert(t *testing.T) { - uu := map[string]struct { - ee, e render.RowEvents - re render.RowEvent - }{ - "add": { - ee: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - re: render.RowEvent{ - Row: render.Row{ID: "D", Fields: render.Fields{"f1", "f2", "f3"}}, - }, - e: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - {Row: render.Row{ID: "D", Fields: render.Fields{"f1", "f2", "f3"}}}, - }, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.ee.Upsert(u.re)) - }) - } -} - -func TestRowEventsCustomize(t *testing.T) { - uu := map[string]struct { - re, e render.RowEvents - cols []int - }{ - "same": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - cols: []int{0, 1, 2}, - e: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - "reverse": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - cols: []int{2, 1, 0}, - e: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"3", "2", "1"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"3", "2", "0"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"3", "2", "10"}}}, - }, - }, - "skip": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - cols: []int{1, 0}, - e: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"2", "1"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"2", "0"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"2", "10"}}}, - }, - }, - "missing": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - cols: []int{1, 0, 4}, - e: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"2", "1", ""}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"2", "0", ""}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"2", "10", ""}}}, - }, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.re.Customize(u.cols)) - }) - } -} - -func TestRowEventsDelete(t *testing.T) { - uu := map[string]struct { - re render.RowEvents - id string - e render.RowEvents - }{ - "first": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - id: "A", - e: render.RowEvents{ - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - "middle": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - id: "B", - e: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - "last": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - id: "C", - e: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - }, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.re.Delete(u.id)) - }) - } -} - -func TestRowEventsSort(t *testing.T) { - uu := map[string]struct { - re render.RowEvents - col int - duration, num, asc bool - capacity bool - e render.RowEvents - }{ - "age_time": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", testTime().Add(20 * time.Second).String()}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", testTime().Add(10 * time.Second).String()}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", testTime().String()}}}, - }, - col: 2, - asc: true, - duration: true, - e: render.RowEvents{ - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", testTime().String()}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", testTime().Add(10 * time.Second).String()}}}, - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", testTime().Add(20 * time.Second).String()}}}, - }, - }, - "col0": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - col: 0, - asc: true, - e: render.RowEvents{ - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - "id_preserve": { - re: render.RowEvents{ - {Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3"}}}, - {Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3"}}}, - {Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3"}}}, - {Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3"}}}, - {Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3"}}}, - {Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3"}}}, - }, - col: 1, - asc: true, - e: render.RowEvents{ - {Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3"}}}, - {Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3"}}}, - {Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3"}}}, - {Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3"}}}, - {Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3"}}}, - {Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3"}}}, - }, - }, - "capacity": { - re: render.RowEvents{ - {Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3", "1Gi"}}}, - {Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3", "1.1G"}}}, - {Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3", "0.5Ti"}}}, - {Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3", "12e6"}}}, - {Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3", "1234"}}}, - {Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3", "0.1Ei"}}}, - }, - col: 3, - asc: true, - capacity: true, - e: render.RowEvents{ - {Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3", "1234"}}}, - {Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3", "12e6"}}}, - {Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3", "1Gi"}}}, - {Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3", "1.1G"}}}, - {Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3", "0.5Ti"}}}, - {Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3", "0.1Ei"}}}, - }, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - u.re.Sort("", u.col, u.duration, u.num, u.capacity, u.asc) - assert.Equal(t, u.e, u.re) - }) - } -} - -func TestRowEventsClone(t *testing.T) { - uu := map[string]struct { - r render.RowEvents - }{ - "empty": { - r: render.RowEvents{}, - }, - "full": { - r: makeRowEvents(), - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - c := u.r.Clone() - assert.Equal(t, len(u.r), len(c)) - if len(u.r) > 0 { - u.r[0].Row.Fields[0] = "blee" - assert.Equal(t, "A", c[0].Row.Fields[0]) - } - }) - } -} - -// Helpers... - -func makeRowEvents() render.RowEvents { - return render.RowEvents{ - {Row: render.Row{ID: "ns1/A", Fields: render.Fields{"A", "2", "3"}}}, - {Row: render.Row{ID: "ns1/B", Fields: render.Fields{"B", "2", "3"}}}, - {Row: render.Row{ID: "ns1/C", Fields: render.Fields{"C", "2", "3"}}}, - {Row: render.Row{ID: "ns2/A", Fields: render.Fields{"A", "2", "3"}}}, - {Row: render.Row{ID: "ns2/B", Fields: render.Fields{"B", "2", "3"}}}, - {Row: render.Row{ID: "ns2/C", Fields: render.Fields{"C", "2", "3"}}}, - } -} diff --git a/internal/render/rs.go b/internal/render/rs.go index b5eb1a3825..85d5dbed3a 100644 --- a/internal/render/rs.go +++ b/internal/render/rs.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -5,6 +8,7 @@ import ( "strconv" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tview" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -17,29 +21,30 @@ type ReplicaSet struct { } // ColorerFunc colors a resource row. -func (r ReplicaSet) ColorerFunc() ColorerFunc { - return DefaultColorer +func (r ReplicaSet) ColorerFunc() model1.ColorerFunc { + return model1.DefaultColorer } // Header returns a header row. -func (ReplicaSet) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "DESIRED", Align: tview.AlignRight}, - HeaderColumn{Name: "CURRENT", Align: tview.AlignRight}, - HeaderColumn{Name: "READY", Align: tview.AlignRight}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (ReplicaSet) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", VS: true}, + model1.HeaderColumn{Name: "DESIRED", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "CURRENT", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "READY", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (r ReplicaSet) Render(o interface{}, ns string, row *Row) error { +func (r ReplicaSet) Render(o interface{}, ns string, row *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected ReplicaSet, but got %T", o) + return fmt.Errorf("expected ReplicaSet, but got %T", o) } var rs appsv1.ReplicaSet err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &rs) @@ -48,15 +53,16 @@ func (r ReplicaSet) Render(o interface{}, ns string, row *Row) error { } row.ID = client.MetaFQN(rs.ObjectMeta) - row.Fields = Fields{ + row.Fields = model1.Fields{ rs.Namespace, rs.Name, + computeVulScore(rs.ObjectMeta, &rs.Spec.Template.Spec), strconv.Itoa(int(*rs.Spec.Replicas)), strconv.Itoa(int(rs.Status.Replicas)), strconv.Itoa(int(rs.Status.ReadyReplicas)), mapToStr(rs.Labels), - asStatus(r.diagnose(rs)), - toAge(rs.GetCreationTimestamp()), + AsStatus(r.diagnose(rs)), + ToAge(rs.GetCreationTimestamp()), } return nil diff --git a/internal/render/rs_test.go b/internal/render/rs_test.go index ef051afb0f..7a84cf38e3 100644 --- a/internal/render/rs_test.go +++ b/internal/render/rs_test.go @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestReplicaSetRender(t *testing.T) { c := render.ReplicaSet{} - r := render.NewRow(4) + r := model1.NewRow(4) assert.NoError(t, c.Render(load(t, "rs"), "", &r)) assert.Equal(t, "icx/icx-db-7d4b578979", r.ID) - assert.Equal(t, render.Fields{"icx", "icx-db-7d4b578979", "1", "1", "1"}, r.Fields[:5]) + assert.Equal(t, model1.Fields{"icx", "icx-db-7d4b578979", "0", "1", "1", "1"}, r.Fields[:6]) } diff --git a/internal/render/sa.go b/internal/render/sa.go index ac728cb1cf..1f463a4e34 100644 --- a/internal/render/sa.go +++ b/internal/render/sa.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -5,6 +8,7 @@ import ( "strconv" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -16,22 +20,22 @@ type ServiceAccount struct { } // Header returns a header row. -func (ServiceAccount) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "SECRET"}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (ServiceAccount) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "SECRET"}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (s ServiceAccount) Render(o interface{}, ns string, r *Row) error { +func (s ServiceAccount) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected ServiceAccount, but got %T", o) + return fmt.Errorf("expected ServiceAccount, but got %T", o) } var sa v1.ServiceAccount err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sa) @@ -40,13 +44,13 @@ func (s ServiceAccount) Render(o interface{}, ns string, r *Row) error { } r.ID = client.MetaFQN(sa.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ sa.Namespace, sa.Name, strconv.Itoa(len(sa.Secrets)), mapToStr(sa.Labels), "", - toAge(sa.GetCreationTimestamp()), + ToAge(sa.GetCreationTimestamp()), } return nil diff --git a/internal/render/sa_test.go b/internal/render/sa_test.go index 31a7b772b8..932ee79895 100644 --- a/internal/render/sa_test.go +++ b/internal/render/sa_test.go @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestServiceAccountRender(t *testing.T) { c := render.ServiceAccount{} - r := render.NewRow(4) + r := model1.NewRow(4) assert.NoError(t, c.Render(load(t, "sa"), "", &r)) assert.Equal(t, "default/blee", r.ID) - assert.Equal(t, render.Fields{"default", "blee", "2"}, r.Fields[:3]) + assert.Equal(t, model1.Fields{"default", "blee", "2"}, r.Fields[:3]) } diff --git a/internal/render/sc.go b/internal/render/sc.go index a2995eab7e..f805fb1000 100644 --- a/internal/render/sc.go +++ b/internal/render/sc.go @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( "fmt" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" storagev1 "k8s.io/api/storage/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -17,24 +21,24 @@ type StorageClass struct { } // Header returns a header row. -func (StorageClass) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "PROVISIONER"}, - HeaderColumn{Name: "RECLAIMPOLICY"}, - HeaderColumn{Name: "VOLUMEBINDINGMODE"}, - HeaderColumn{Name: "ALLOWVOLUMEEXPANSION"}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (StorageClass) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "PROVISIONER"}, + model1.HeaderColumn{Name: "RECLAIMPOLICY"}, + model1.HeaderColumn{Name: "VOLUMEBINDINGMODE"}, + model1.HeaderColumn{Name: "ALLOWVOLUMEEXPANSION"}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (s StorageClass) Render(o interface{}, ns string, r *Row) error { +func (s StorageClass) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected StorageClass, but got %T", o) + return fmt.Errorf("expected StorageClass, but got %T", o) } var sc storagev1.StorageClass err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sc) @@ -43,7 +47,7 @@ func (s StorageClass) Render(o interface{}, ns string, r *Row) error { } r.ID = client.FQN(client.ClusterScope, sc.ObjectMeta.Name) - r.Fields = Fields{ + r.Fields = model1.Fields{ s.nameWithDefault(sc.ObjectMeta), sc.Provisioner, strPtrToStr((*string)(sc.ReclaimPolicy)), @@ -51,7 +55,7 @@ func (s StorageClass) Render(o interface{}, ns string, r *Row) error { boolPtrToStr(sc.AllowVolumeExpansion), mapToStr(sc.Labels), "", - toAge(sc.GetCreationTimestamp()), + ToAge(sc.GetCreationTimestamp()), } return nil diff --git a/internal/render/sc_test.go b/internal/render/sc_test.go index 60cd56f242..c588313610 100644 --- a/internal/render/sc_test.go +++ b/internal/render/sc_test.go @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestStorageClassRender(t *testing.T) { c := render.StorageClass{} - r := render.NewRow(4) + r := model1.NewRow(4) assert.NoError(t, c.Render(load(t, "sc"), "", &r)) assert.Equal(t, "-/standard", r.ID) - assert.Equal(t, render.Fields{"standard (default)", "kubernetes.io/gce-pd", "Delete", "Immediate", "true"}, r.Fields[:5]) + assert.Equal(t, model1.Fields{"standard (default)", "kubernetes.io/gce-pd", "Delete", "Immediate", "true"}, r.Fields[:5]) } diff --git a/internal/render/screen_dump.go b/internal/render/screen_dump.go index 230303a1bf..8193c612dc 100644 --- a/internal/render/screen_dump.go +++ b/internal/render/screen_dump.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -6,9 +9,11 @@ import ( "path/filepath" "time" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/duration" ) // ScreenDump renders a screendumps to screen. @@ -17,31 +22,31 @@ type ScreenDump struct { } // ColorerFunc colors a resource row. -func (ScreenDump) ColorerFunc() ColorerFunc { - return func(ns string, _ Header, re RowEvent) tcell.Color { +func (ScreenDump) ColorerFunc() model1.ColorerFunc { + return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color { return tcell.ColorNavajoWhite } } // Header returns a header row. -func (ScreenDump) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "DIR"}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (ScreenDump) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "DIR"}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (b ScreenDump) Render(o interface{}, ns string, r *Row) error { +func (b ScreenDump) Render(o interface{}, ns string, r *model1.Row) error { f, ok := o.(FileRes) if !ok { return fmt.Errorf("expecting screendumper, but got %T", o) } r.ID = filepath.Join(f.Dir, f.File.Name()) - r.Fields = Fields{ + r.Fields = model1.Fields{ f.File.Name(), f.Dir, "", @@ -55,7 +60,7 @@ func (b ScreenDump) Render(o interface{}, ns string, r *Row) error { // Helpers... func timeToAge(timestamp time.Time) string { - return time.Since(timestamp).String() + return duration.HumanDuration(time.Since(timestamp)) } // FileRes represents a file resource. diff --git a/internal/render/screen_dump_test.go b/internal/render/screen_dump_test.go index b7737a90de..55c82f3887 100644 --- a/internal/render/screen_dump_test.go +++ b/internal/render/screen_dump_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( @@ -5,13 +8,14 @@ import ( "testing" "time" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestScreenDumpRender(t *testing.T) { var s render.ScreenDump - var r render.Row + var r model1.Row o := render.FileRes{ File: fileInfo{}, Dir: "fred/blee", @@ -19,7 +23,7 @@ func TestScreenDumpRender(t *testing.T) { assert.Nil(t, s.Render(o, "fred", &r)) assert.Equal(t, "fred/blee/bob", r.ID) - assert.Equal(t, render.Fields{ + assert.Equal(t, model1.Fields{ "bob", "fred/blee", "", diff --git a/internal/render/secret.go b/internal/render/secret.go new file mode 100644 index 0000000000..d1d3aad9b0 --- /dev/null +++ b/internal/render/secret.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package render + +import ( + "fmt" + "strconv" + + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// Secret renders a K8s Secret to screen. +type Secret struct { + Base +} + +// Header returns a header rbw. +func (Secret) Header(string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "TYPE"}, + model1.HeaderColumn{Name: "DATA"}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, + } +} + +// Render renders a K8s resource to screen. +func (n Secret) Render(o interface{}, _ string, r *model1.Row) error { + raw, ok := o.(*unstructured.Unstructured) + if !ok { + return fmt.Errorf("expected Secret, but got %T", o) + } + var sec v1.Secret + err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sec) + if err != nil { + return err + } + + r.ID = client.FQN(sec.Namespace, sec.Name) + r.Fields = model1.Fields{ + sec.Namespace, + sec.Name, + string(sec.Type), + strconv.Itoa(len(sec.Data)), + "", + ToAge(raw.GetCreationTimestamp()), + } + + return nil +} diff --git a/internal/render/sts.go b/internal/render/sts.go index 58b8f2ba05..c83ffa7ba6 100644 --- a/internal/render/sts.go +++ b/internal/render/sts.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -5,6 +8,7 @@ import ( "strconv" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -16,26 +20,27 @@ type StatefulSet struct { } // Header returns a header row. -func (StatefulSet) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "READY"}, - HeaderColumn{Name: "SELECTOR", Wide: true}, - HeaderColumn{Name: "SERVICE"}, - HeaderColumn{Name: "CONTAINERS", Wide: true}, - HeaderColumn{Name: "IMAGES", Wide: true}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (StatefulSet) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "VS", VS: true}, + model1.HeaderColumn{Name: "READY"}, + model1.HeaderColumn{Name: "SELECTOR", Wide: true}, + model1.HeaderColumn{Name: "SERVICE"}, + model1.HeaderColumn{Name: "CONTAINERS", Wide: true}, + model1.HeaderColumn{Name: "IMAGES", Wide: true}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (s StatefulSet) Render(o interface{}, ns string, r *Row) error { +func (s StatefulSet) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected StatefulSet, but got %T", o) + return fmt.Errorf("expected StatefulSet, but got %T", o) } var sts appsv1.StatefulSet err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sts) @@ -44,25 +49,30 @@ func (s StatefulSet) Render(o interface{}, ns string, r *Row) error { } r.ID = client.MetaFQN(sts.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ sts.Namespace, sts.Name, + computeVulScore(sts.ObjectMeta, &sts.Spec.Template.Spec), strconv.Itoa(int(sts.Status.ReadyReplicas)) + "/" + strconv.Itoa(int(sts.Status.Replicas)), asSelector(sts.Spec.Selector), na(sts.Spec.ServiceName), podContainerNames(sts.Spec.Template.Spec, true), podImageNames(sts.Spec.Template.Spec, true), mapToStr(sts.Labels), - asStatus(s.diagnose(sts.Status.Replicas, sts.Status.ReadyReplicas)), - toAge(sts.GetCreationTimestamp()), + AsStatus(s.diagnose(sts.Spec.Replicas, sts.Status.Replicas, sts.Status.ReadyReplicas)), + ToAge(sts.GetCreationTimestamp()), } return nil } -func (StatefulSet) diagnose(d, r int32) error { +func (StatefulSet) diagnose(w *int32, d, r int32) error { if d != r { - return fmt.Errorf("desiring %d replicas got %d available", d, r) + return fmt.Errorf("desired %d replicas got %d available", d, r) + } + if w != nil && *w != r { + return fmt.Errorf("want %d replicas got %d available", *w, r) } + return nil } diff --git a/internal/render/sts_test.go b/internal/render/sts_test.go index 600daa9384..d8a4edc8a4 100644 --- a/internal/render/sts_test.go +++ b/internal/render/sts_test.go @@ -1,17 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestStatefulSetRender(t *testing.T) { c := render.StatefulSet{} - r := render.NewRow(4) + r := model1.NewRow(4) assert.Nil(t, c.Render(load(t, "sts"), "", &r)) assert.Equal(t, "default/nginx-sts", r.ID) - assert.Equal(t, render.Fields{"default", "nginx-sts", "4/4", "app=nginx-sts", "nginx-sts", "nginx", "k8s.gcr.io/nginx-slim:0.8", "app=nginx-sts", ""}, r.Fields[:len(r.Fields)-1]) + assert.Equal(t, model1.Fields{"default", "nginx-sts", "0", "4/4", "app=nginx-sts", "nginx-sts", "nginx", "k8s.gcr.io/nginx-slim:0.8", "app=nginx-sts", ""}, r.Fields[:len(r.Fields)-1]) } diff --git a/internal/render/subject.go b/internal/render/subject.go index 07b5c54867..af3c0a6173 100644 --- a/internal/render/subject.go +++ b/internal/render/subject.go @@ -1,8 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( "fmt" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/tcell/v2" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -14,31 +18,31 @@ type Subject struct { } // ColorerFunc colors a resource row. -func (Subject) ColorerFunc() ColorerFunc { - return func(ns string, _ Header, re RowEvent) tcell.Color { +func (Subject) ColorerFunc() model1.ColorerFunc { + return func(ns string, _ model1.Header, re *model1.RowEvent) tcell.Color { return tcell.ColorMediumSpringGreen } } // Header returns a header row. -func (Subject) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "KIND"}, - HeaderColumn{Name: "FIRST LOCATION"}, - HeaderColumn{Name: "VALID", Wide: true}, +func (Subject) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "KIND"}, + model1.HeaderColumn{Name: "FIRST LOCATION"}, + model1.HeaderColumn{Name: "VALID", Wide: true}, } } // Render renders a K8s resource to screen. -func (s Subject) Render(o interface{}, ns string, r *Row) error { +func (s Subject) Render(o interface{}, ns string, r *model1.Row) error { res, ok := o.(SubjectRes) if !ok { - return fmt.Errorf("Expected SubjectRes, but got %T", s) + return fmt.Errorf("expected SubjectRes, but got %T", s) } r.ID = res.Name - r.Fields = Fields{ + r.Fields = model1.Fields{ res.Name, res.Kind, res.FirstLocation, diff --git a/internal/render/svc.go b/internal/render/svc.go index 6c72d3615b..73081cadf0 100644 --- a/internal/render/svc.go +++ b/internal/render/svc.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render import ( @@ -7,6 +10,7 @@ import ( "strings" "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -18,26 +22,26 @@ type Service struct { } // Header returns a header row. -func (Service) Header(ns string) Header { - return Header{ - HeaderColumn{Name: "NAMESPACE"}, - HeaderColumn{Name: "NAME"}, - HeaderColumn{Name: "TYPE"}, - HeaderColumn{Name: "CLUSTER-IP"}, - HeaderColumn{Name: "EXTERNAL-IP"}, - HeaderColumn{Name: "SELECTOR", Wide: true}, - HeaderColumn{Name: "PORTS", Wide: false}, - HeaderColumn{Name: "LABELS", Wide: true}, - HeaderColumn{Name: "VALID", Wide: true}, - HeaderColumn{Name: "AGE", Time: true}, +func (Service) Header(ns string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "TYPE"}, + model1.HeaderColumn{Name: "CLUSTER-IP"}, + model1.HeaderColumn{Name: "EXTERNAL-IP"}, + model1.HeaderColumn{Name: "SELECTOR", Wide: true}, + model1.HeaderColumn{Name: "PORTS", Wide: false}, + model1.HeaderColumn{Name: "LABELS", Wide: true}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, } } // Render renders a K8s resource to screen. -func (s Service) Render(o interface{}, ns string, r *Row) error { +func (s Service) Render(o interface{}, ns string, r *model1.Row) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected Service, but got %T", o) + return fmt.Errorf("expected Service, but got %T", o) } var svc v1.Service err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &svc) @@ -46,7 +50,7 @@ func (s Service) Render(o interface{}, ns string, r *Row) error { } r.ID = client.MetaFQN(svc.ObjectMeta) - r.Fields = Fields{ + r.Fields = model1.Fields{ svc.Namespace, svc.ObjectMeta.Name, string(svc.Spec.Type), @@ -55,8 +59,8 @@ func (s Service) Render(o interface{}, ns string, r *Row) error { mapToStr(svc.Spec.Selector), ToPorts(svc.Spec.Ports), mapToStr(svc.Labels), - asStatus(s.diagnose()), - toAge(svc.GetCreationTimestamp()), + AsStatus(s.diagnose()), + ToAge(svc.GetCreationTimestamp()), } return nil diff --git a/internal/render/svc_test.go b/internal/render/svc_test.go index 51df052236..6a1ea62fe4 100644 --- a/internal/render/svc_test.go +++ b/internal/render/svc_test.go @@ -1,24 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render_test import ( "testing" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) func TestServiceRender(t *testing.T) { c := render.Service{} - r := render.NewRow(4) + r := model1.NewRow(4) assert.NoError(t, c.Render(load(t, "svc"), "", &r)) assert.Equal(t, "default/dictionary1", r.ID) - assert.Equal(t, render.Fields{"default", "dictionary1", "ClusterIP", "10.47.248.116", "", "app=dictionary1", "http:4001►0"}, r.Fields[:7]) + assert.Equal(t, model1.Fields{"default", "dictionary1", "ClusterIP", "10.47.248.116", "", "app=dictionary1", "http:4001►0"}, r.Fields[:7]) } func BenchmarkSvcRender(b *testing.B) { var svc render.Service - r := render.NewRow(4) + r := model1.NewRow(4) s := load(b, "svc") b.ResetTimer() b.ReportAllocs() diff --git a/internal/render/table_data.go b/internal/render/table_data.go deleted file mode 100644 index c3f3b12b7a..0000000000 --- a/internal/render/table_data.go +++ /dev/null @@ -1,147 +0,0 @@ -package render - -import ( - "sync" - - "github.com/derailed/k9s/internal/client" -) - -// TableData tracks a K8s resource for tabular display. -type TableData struct { - Header Header - RowEvents RowEvents - Namespace string - mx sync.RWMutex -} - -// NewTableData returns a new table. -func NewTableData() *TableData { - return &TableData{} -} - -// Empty checks if there are no entries. -func (t *TableData) Empty() bool { - t.mx.RLock() - defer t.mx.RUnlock() - - return len(t.RowEvents) == 0 -} - -// Count returns the number of entries. -func (t *TableData) Count() int { - t.mx.RLock() - defer t.mx.RUnlock() - - return len(t.RowEvents) -} - -// IndexOfHeader return the index of the header. -func (t *TableData) IndexOfHeader(h string) int { - return t.Header.IndexOf(h, false) -} - -// Labelize prints out specific label columns. -func (t *TableData) Labelize(labels []string) *TableData { - labelCol := t.Header.IndexOf("LABELS", true) - cols := []int{0, 1} - if client.IsNamespaced(t.Namespace) { - cols = cols[1:] - } - data := TableData{ - Namespace: t.Namespace, - Header: t.Header.Labelize(cols, labelCol, t.RowEvents), - } - data.RowEvents = t.RowEvents.Labelize(cols, labelCol, labels) - - return &data -} - -// Customize returns a new model with customized column layout. -func (t *TableData) Customize(cols []string, wide bool) *TableData { - res := TableData{ - Namespace: t.Namespace, - Header: t.Header.Customize(cols, wide), - } - ids := t.Header.MapIndices(cols, wide) - res.RowEvents = t.RowEvents.Customize(ids) - - return &res -} - -// Clear clears out the entire table. -func (t *TableData) Clear() { - t.Header, t.RowEvents = Header{}, RowEvents{} -} - -// Clone returns a copy of the table. -func (t *TableData) Clone() *TableData { - return &TableData{ - Header: t.Header.Clone(), - RowEvents: t.RowEvents.Clone(), - Namespace: t.Namespace, - } -} - -// SetHeader sets table header. -func (t *TableData) SetHeader(ns string, h Header) { - t.Namespace, t.Header = ns, h -} - -// Update computes row deltas and update the table data. -func (t *TableData) Update(rows Rows) { - empty := t.Empty() - kk := make(map[string]struct{}, len(rows)) - t.mx.Lock() - { - var blankDelta DeltaRow - for _, row := range rows { - kk[row.ID] = struct{}{} - if empty { - t.RowEvents = append(t.RowEvents, NewRowEvent(EventAdd, row)) - continue - } - if index, ok := t.RowEvents.FindIndex(row.ID); ok { - delta := NewDeltaRow(t.RowEvents[index].Row, row, t.Header) - if delta.IsBlank() { - t.RowEvents[index].Kind, t.RowEvents[index].Deltas = EventUnchanged, blankDelta - t.RowEvents[index].Row = row - } else { - t.RowEvents[index] = NewRowEventWithDeltas(row, delta) - } - continue - } - t.RowEvents = append(t.RowEvents, NewRowEvent(EventAdd, row)) - } - } - t.mx.Unlock() - - if !empty { - t.Delete(kk) - } -} - -// Delete removes items in cache that are no longer valid. -func (t *TableData) Delete(newKeys map[string]struct{}) { - t.mx.Lock() - { - var victims []string - for _, re := range t.RowEvents { - if _, ok := newKeys[re.Row.ID]; !ok { - victims = append(victims, re.Row.ID) - } - } - for _, id := range victims { - t.RowEvents = t.RowEvents.Delete(id) - } - } - t.mx.Unlock() -} - -// Diff checks if two tables are equal. -func (t *TableData) Diff(t2 *TableData) bool { - if t2 == nil || t.Namespace != t2.Namespace || t.Header.Diff(t2.Header) { - return true - } - - return t.RowEvents.Diff(t2.RowEvents, t.Header.IndexOf("AGE", true)) -} diff --git a/internal/render/table_data_test.go b/internal/render/table_data_test.go deleted file mode 100644 index efa81fe0e0..0000000000 --- a/internal/render/table_data_test.go +++ /dev/null @@ -1,393 +0,0 @@ -package render_test - -import ( - "testing" - - "github.com/derailed/k9s/internal/render" - "github.com/stretchr/testify/assert" -) - -func TestTableDataCustomize(t *testing.T) { - uu := map[string]struct { - t1, e *render.TableData - cols []string - wide bool - }{ - "same": { - t1: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - cols: []string{"A", "B", "C"}, - e: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - }, - "wide-col": { - t1: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - cols: []string{"A", "B", "C"}, - e: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: false}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - }, - "wide": { - t1: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B", Wide: true}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - wide: true, - cols: []string{"A", "C"}, - e: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "C"}, - render.HeaderColumn{Name: "B", Wide: true}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "3", "2"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "3", "2"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "3", "2"}}}, - }, - }, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.t1.Customize(u.cols, u.wide)) - }) - } -} - -func TestTableDataDiff(t *testing.T) { - uu := map[string]struct { - t1, t2 *render.TableData - e bool - }{ - "empty": { - t1: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - e: true, - }, - "same": { - t1: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - t2: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - }, - "ns-diff": { - t1: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - t2: &render.TableData{ - Namespace: "blee", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - e: true, - }, - "header-diff": { - t1: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "D"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - t2: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - e: true, - }, - "row-diff": { - t1: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - t2: &render.TableData{ - Namespace: "fred", - Header: render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - }, - RowEvents: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"100", "2", "3"}}}, - }, - }, - e: true, - }, - } - - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, u.t1.Diff(u.t2)) - }) - } -} - -func TestTableDataUpdate(t *testing.T) { - uu := map[string]struct { - re render.RowEvents - rr render.Rows - e render.RowEvents - }{ - "no-change": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - rr: render.Rows{ - render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}, - render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}, - }, - e: render.RowEvents{ - {Kind: render.EventUnchanged, Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Kind: render.EventUnchanged, Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Kind: render.EventUnchanged, Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - "add": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - rr: render.Rows{ - render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}, - render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}, - render.Row{ID: "D", Fields: render.Fields{"10", "2", "3"}}, - }, - e: render.RowEvents{ - {Kind: render.EventUnchanged, Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Kind: render.EventUnchanged, Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Kind: render.EventUnchanged, Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - {Kind: render.EventAdd, Row: render.Row{ID: "D", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - "delete": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - rr: render.Rows{ - render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}, - render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}, - }, - e: render.RowEvents{ - {Kind: render.EventUnchanged, Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Kind: render.EventUnchanged, Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - "update": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - rr: render.Rows{ - render.Row{ID: "A", Fields: render.Fields{"10", "2", "3"}}, - render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}, - render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}, - }, - e: render.RowEvents{ - { - Kind: render.EventUpdate, - Row: render.Row{ID: "A", Fields: render.Fields{"10", "2", "3"}}, - Deltas: render.DeltaRow{"1", "", ""}, - }, - {Kind: render.EventUnchanged, Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Kind: render.EventUnchanged, Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - } - - var table render.TableData - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - table.RowEvents = u.re - table.Update(u.rr) - assert.Equal(t, u.e, table.RowEvents) - }) - } -} - -func TestTableDataDelete(t *testing.T) { - uu := map[string]struct { - re render.RowEvents - kk map[string]struct{} - e render.RowEvents - }{ - "ordered": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - kk: map[string]struct{}{"A": {}, "C": {}}, - e: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - "unordered": { - re: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "B", Fields: render.Fields{"0", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - {Row: render.Row{ID: "D", Fields: render.Fields{"10", "2", "3"}}}, - }, - kk: map[string]struct{}{"C": {}, "A": {}}, - e: render.RowEvents{ - {Row: render.Row{ID: "A", Fields: render.Fields{"1", "2", "3"}}}, - {Row: render.Row{ID: "C", Fields: render.Fields{"10", "2", "3"}}}, - }, - }, - } - - var table render.TableData - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - table.RowEvents = u.re - table.Delete(u.kk) - assert.Equal(t, u.e, table.RowEvents) - }) - } -} diff --git a/internal/render/testdata/p1.json b/internal/render/testdata/p1.json new file mode 100644 index 0000000000..ea8d8dad73 --- /dev/null +++ b/internal/render/testdata/p1.json @@ -0,0 +1,146 @@ +{ + "apiVersion": "v1", + "kind": "Pod", + "metadata": { + "annotations": { + "kubectl.kubernetes.io/restartedAt": "2019-12-31T12:26:47-07:00" + }, + "creationTimestamp": "2019-12-31T19:27:22Z", + "generateName": "nginx-7fb78fb6d8-", + "labels": { + "app": "nginx", + "pod-template-hash": "7fb78fb6d8" + }, + "name": "nginx-7fb78fb6d8-2w75j", + "namespace": "default", + "ownerReferences": [ + { + "apiVersion": "apps/v1", + "blockOwnerDeletion": true, + "controller": true, + "kind": "ReplicaSet", + "name": "nginx-7fb78fb6d8", + "uid": "7ccd0600-2c03-11ea-883f-42010a800044" + } + ], + "resourceVersion": "87290191", + "selfLink": "/api/v1/namespaces/default/pods/nginx-7fb78fb6d8-2w75j", + "uid": "91bb1cf2-2c03-11ea-883f-42010a800044" + }, + "spec": { + "containers": [ + { + "image": "k8s.gcr.io/nginx-slim:0.8", + "imagePullPolicy": "IfNotPresent", + "name": "nginx", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + } + ], + "resources": { + "limits": { + "cpu": "200m", + "memory": "20Mi" + }, + "requests": { + "cpu": "200m", + "memory": "20Mi" + } + }, + "terminationMessagePath": "/dev/termination-log", + "terminationMessagePolicy": "File", + "volumeMounts": [ + { + "mountPath": "/var/run/secrets/kubernetes.io/serviceaccount", + "name": "default-token-dsl46", + "readOnly": true + } + ] + } + ], + "dnsPolicy": "ClusterFirst", + "enableServiceLinks": true, + "nodeName": "gke-k9s-default-pool-0fa2fb89-lbtf", + "priority": 0, + "restartPolicy": "Always", + "schedulerName": "default-scheduler", + "securityContext": {}, + "serviceAccount": "default", + "serviceAccountName": "default", + "terminationGracePeriodSeconds": 30, + "tolerations": [ + { + "effect": "NoExecute", + "key": "node.kubernetes.io/not-ready", + "operator": "Exists", + "tolerationSeconds": 300 + }, + { + "effect": "NoExecute", + "key": "node.kubernetes.io/unreachable", + "operator": "Exists", + "tolerationSeconds": 300 + } + ], + "volumes": [ + { + "name": "default-token-dsl46", + "secret": { + "defaultMode": 420, + "secretName": "default-token-dsl46" + } + } + ] + }, + "status": { + "conditions": [ + { + "lastProbeTime": null, + "lastTransitionTime": "2019-12-31T19:27:23Z", + "status": "True", + "type": "Initialized" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-12-31T19:27:25Z", + "status": "True", + "type": "Ready" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-12-31T19:27:25Z", + "status": "True", + "type": "ContainersReady" + }, + { + "lastProbeTime": null, + "lastTransitionTime": "2019-12-31T19:27:22Z", + "status": "True", + "type": "PodScheduled" + } + ], + "containerStatuses": [ + { + "containerID": "docker://90e0abf7a779dd76d36038883312baed57a8351428a1d6340df3cff698f51809", + "image": "k8s.gcr.io/nginx-slim:0.8", + "imageID": "docker-pullable://k8s.gcr.io/nginx-slim@sha256:8b4501fe0fe221df663c22e16539f399e89594552f400408303c42f3dd8d0e52", + "lastState": {}, + "name": "nginx", + "ready": true, + "restartCount": 0, + "state": { + "running": { + "startedAt": "2019-12-31T19:27:24Z" + } + } + } + ], + "hostIP": "10.128.0.15", + "phase": "Running", + "podIP": "10.44.0.229", + "qosClass": "Guaranteed", + "startTime": "2019-12-31T19:27:23Z" + } +} \ No newline at end of file diff --git a/internal/render/testdata/pdb.json b/internal/render/testdata/pdb.json index 0e4a36010d..4753694d0e 100644 --- a/internal/render/testdata/pdb.json +++ b/internal/render/testdata/pdb.json @@ -1,16 +1,16 @@ { - "apiVersion": "policy/v1beta1", + "apiVersion": "policy/v1", "kind": "PodDisruptionBudget", "metadata": { "annotations": { - "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"policy/v1beta1\",\"kind\":\"PodDisruptionBudget\",\"metadata\":{\"annotations\":{},\"name\":\"fred\",\"namespace\":\"default\"},\"spec\":{\"minAvailable\":2,\"selector\":{\"matchLabels\":{\"app\":\"nginx\"}}}}\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" }, "creationTimestamp": "2019-08-31T03:48:10Z", "generation": 1, "name": "fred", "namespace": "default", "resourceVersion": "49885429", - "selfLink": "/apis/policy/v1beta1/namespaces/default/poddisruptionbudgets/fred", + "selfLink": "/apis/policy/v1/namespaces/default/poddisruptionbudgets/fred", "uid": "26b6cf70-cba2-11e9-990f-42010a800218" }, "spec": { diff --git a/internal/render/types.go b/internal/render/types.go index 9f5b1ca0a2..3b1c37aebb 100644 --- a/internal/render/types.go +++ b/internal/render/types.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package render const ( diff --git a/internal/render/workload.go b/internal/render/workload.go new file mode 100644 index 0000000000..7a3a2645ec --- /dev/null +++ b/internal/render/workload.go @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package render + +import ( + "fmt" + "strings" + + "github.com/derailed/k9s/internal/model1" + "github.com/derailed/tcell/v2" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// Workload renders a workload to screen. +type Workload struct { + Base +} + +// ColorerFunc colors a resource row. +func (n Workload) ColorerFunc() model1.ColorerFunc { + return func(ns string, h model1.Header, re *model1.RowEvent) tcell.Color { + c := model1.DefaultColorer(ns, h, re) + + idx, ok := h.IndexOf("STATUS", true) + if !ok { + return c + } + status := strings.TrimSpace(re.Row.Fields[idx]) + if status == "DEGRADED" { + c = model1.PendingColor + } + + return c + } +} + +// Header returns a header rbw. +func (Workload) Header(string) model1.Header { + return model1.Header{ + model1.HeaderColumn{Name: "KIND"}, + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME"}, + model1.HeaderColumn{Name: "STATUS"}, + model1.HeaderColumn{Name: "READY"}, + model1.HeaderColumn{Name: "VALID", Wide: true}, + model1.HeaderColumn{Name: "AGE", Time: true}, + } +} + +// Render renders a K8s resource to screen. +func (n Workload) Render(o interface{}, _ string, r *model1.Row) error { + res, ok := o.(*WorkloadRes) + if !ok { + return fmt.Errorf("expected allRes but got %T", o) + } + + r.ID = fmt.Sprintf("%s|%s|%s", res.Row.Cells[0].(string), res.Row.Cells[1].(string), res.Row.Cells[2].(string)) + r.Fields = model1.Fields{ + res.Row.Cells[0].(string), + res.Row.Cells[1].(string), + res.Row.Cells[2].(string), + res.Row.Cells[3].(string), + res.Row.Cells[4].(string), + res.Row.Cells[5].(string), + ToAge(res.Row.Cells[6].(metav1.Time)), + } + + return nil +} + +type WorkloadRes struct { + Row metav1.TableRow +} + +// GetObjectKind returns a schema object. +func (a *WorkloadRes) GetObjectKind() schema.ObjectKind { + return nil +} + +// DeepCopyObject returns a container copy. +func (a *WorkloadRes) DeepCopyObject() runtime.Object { + return a +} diff --git a/internal/tchart/component.go b/internal/tchart/component.go index c86e3cb2f1..b557631e25 100644 --- a/internal/tchart/component.go +++ b/internal/tchart/component.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package tchart import ( diff --git a/internal/tchart/component_int_test.go b/internal/tchart/component_int_test.go index ec01a3817e..22945ac5bc 100644 --- a/internal/tchart/component_int_test.go +++ b/internal/tchart/component_int_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package tchart import ( diff --git a/internal/tchart/component_test.go b/internal/tchart/component_test.go index eee63a60f2..07a89c5a48 100644 --- a/internal/tchart/component_test.go +++ b/internal/tchart/component_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package tchart_test import ( diff --git a/internal/tchart/dot_matrix.go b/internal/tchart/dot_matrix.go index cfa781f204..ae6b1bccda 100644 --- a/internal/tchart/dot_matrix.go +++ b/internal/tchart/dot_matrix.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package tchart import ( diff --git a/internal/tchart/dot_matrix_test.go b/internal/tchart/dot_matrix_test.go index ad9cf7df6f..df1c85d208 100644 --- a/internal/tchart/dot_matrix_test.go +++ b/internal/tchart/dot_matrix_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package tchart_test import ( diff --git a/internal/tchart/gauge.go b/internal/tchart/gauge.go index f2da693145..5b83052839 100644 --- a/internal/tchart/gauge.go +++ b/internal/tchart/gauge.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package tchart import ( diff --git a/internal/tchart/gauge_int_test.go b/internal/tchart/gauge_int_test.go index a335eeea4b..b9f191601a 100644 --- a/internal/tchart/gauge_int_test.go +++ b/internal/tchart/gauge_int_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package tchart import ( diff --git a/internal/tchart/gauge_test.go b/internal/tchart/gauge_test.go index 1843947f4e..8708402adf 100644 --- a/internal/tchart/gauge_test.go +++ b/internal/tchart/gauge_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package tchart_test import ( diff --git a/internal/tchart/sparkline.go b/internal/tchart/sparkline.go index ce304f1303..2df126b008 100644 --- a/internal/tchart/sparkline.go +++ b/internal/tchart/sparkline.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package tchart import ( diff --git a/internal/tchart/sparkline_int_test.go b/internal/tchart/sparkline_int_test.go index 36d516124f..14f0cd9b54 100644 --- a/internal/tchart/sparkline_int_test.go +++ b/internal/tchart/sparkline_int_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package tchart import ( diff --git a/internal/ui/action.go b/internal/ui/action.go index 1bf9e0724e..f270e67ba7 100644 --- a/internal/ui/action.go +++ b/internal/ui/action.go @@ -1,7 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( "sort" + "sync" "github.com/derailed/k9s/internal/model" "github.com/derailed/tcell/v2" @@ -9,64 +13,190 @@ import ( ) type ( + // RangeFn represents a range iteration callback. + RangeFn func(tcell.Key, KeyAction) + // ActionHandler handles a keyboard command. ActionHandler func(*tcell.EventKey) *tcell.EventKey + // ActionOpts tracks various action options. + ActionOpts struct { + Visible bool + Shared bool + Plugin bool + HotKey bool + Dangerous bool + } + // KeyAction represents a keyboard action. KeyAction struct { Description string Action ActionHandler - Visible bool - Shared bool + Opts ActionOpts } + // KeyMap tracks key to action mappings. + KeyMap map[tcell.Key]KeyAction + // KeyActions tracks mappings between keystrokes and actions. - KeyActions map[tcell.Key]KeyAction + KeyActions struct { + actions KeyMap + mx sync.RWMutex + } ) // NewKeyAction returns a new keyboard action. -func NewKeyAction(d string, a ActionHandler, display bool) KeyAction { - return KeyAction{Description: d, Action: a, Visible: display} +func NewKeyAction(d string, a ActionHandler, visible bool) KeyAction { + return NewKeyActionWithOpts(d, a, ActionOpts{ + Visible: visible, + }) } // NewSharedKeyAction returns a new shared keyboard action. -func NewSharedKeyAction(d string, a ActionHandler, display bool) KeyAction { - return KeyAction{Description: d, Action: a, Visible: display, Shared: true} +func NewSharedKeyAction(d string, a ActionHandler, visible bool) KeyAction { + return NewKeyActionWithOpts(d, a, ActionOpts{ + Visible: visible, + Shared: true, + }) +} + +// NewKeyActionWithOpts returns a new keyboard action. +func NewKeyActionWithOpts(d string, a ActionHandler, opts ActionOpts) KeyAction { + return KeyAction{ + Description: d, + Action: a, + Opts: opts, + } +} + +// NewKeyActions returns a new instance. +func NewKeyActions() *KeyActions { + return &KeyActions{ + actions: make(map[tcell.Key]KeyAction), + } } -// Add sets up keyboard action listener. -func (a KeyActions) Add(aa KeyActions) { +// NewKeyActionsFromMap construct actions from key map. +func NewKeyActionsFromMap(mm KeyMap) *KeyActions { + return &KeyActions{actions: mm} +} + +// Get fetches an action given a key. +func (a *KeyActions) Get(key tcell.Key) (KeyAction, bool) { + a.mx.RLock() + defer a.mx.RUnlock() + + v, ok := a.actions[key] + + return v, ok +} + +// Len returns action mapping count. +func (a *KeyActions) Len() int { + a.mx.RLock() + defer a.mx.RUnlock() + + return len(a.actions) +} + +// Reset clears out actions. +func (a *KeyActions) Reset(aa *KeyActions) { + a.Clear() + a.Merge(aa) +} + +// Range ranges over all actions and triggers a given function. +func (a *KeyActions) Range(f RangeFn) { + var km KeyMap + a.mx.RLock() + { + km = a.actions + } + a.mx.RUnlock() + + for k, v := range km { + f(k, v) + } +} + +// Add adds a new key action. +func (a *KeyActions) Add(k tcell.Key, ka KeyAction) { + a.mx.Lock() + defer a.mx.Unlock() + + a.actions[k] = ka +} + +// Bulk bulk insert key mappings. +func (a *KeyActions) Bulk(aa KeyMap) { + a.mx.Lock() + defer a.mx.Unlock() + for k, v := range aa { - a[k] = v + a.actions[k] = v + } +} + +// Merge merges given actions into existing set. +func (a *KeyActions) Merge(aa *KeyActions) { + a.mx.Lock() + defer a.mx.Unlock() + + for k, v := range aa.actions { + a.actions[k] = v } } // Clear remove all actions. -func (a KeyActions) Clear() { - for k := range a { - delete(a, k) +func (a *KeyActions) Clear() { + a.mx.Lock() + defer a.mx.Unlock() + + for k := range a.actions { + delete(a.actions, k) + } +} + +// ClearDanger remove all dangerous actions. +func (a *KeyActions) ClearDanger() { + a.mx.Lock() + defer a.mx.Unlock() + + for k, v := range a.actions { + if v.Opts.Dangerous { + delete(a.actions, k) + } } } // Set replace actions with new ones. -func (a KeyActions) Set(aa KeyActions) { - for k, v := range aa { - a[k] = v +func (a *KeyActions) Set(aa *KeyActions) { + a.mx.Lock() + defer a.mx.Unlock() + + for k, v := range aa.actions { + a.actions[k] = v } } // Delete deletes actions by the given keys. -func (a KeyActions) Delete(kk ...tcell.Key) { +func (a *KeyActions) Delete(kk ...tcell.Key) { + a.mx.Lock() + defer a.mx.Unlock() + for _, k := range kk { - delete(a, k) + delete(a.actions, k) } } // Hints returns a collection of hints. -func (a KeyActions) Hints() model.MenuHints { - kk := make([]int, 0, len(a)) - for k := range a { - if !a[k].Shared { +func (a *KeyActions) Hints() model.MenuHints { + a.mx.RLock() + defer a.mx.RUnlock() + + kk := make([]int, 0, len(a.actions)) + for k := range a.actions { + if !a.actions[k].Opts.Shared { kk = append(kk, int(k)) } } @@ -78,13 +208,14 @@ func (a KeyActions) Hints() model.MenuHints { hh = append(hh, model.MenuHint{ Mnemonic: name, - Description: a[tcell.Key(k)].Description, - Visible: a[tcell.Key(k)].Visible, + Description: a.actions[tcell.Key(k)].Description, + Visible: a.actions[tcell.Key(k)].Opts.Visible, }, ) } else { log.Error().Msgf("Unable to locate KeyName for %#v", k) } } + return hh } diff --git a/internal/ui/action_test.go b/internal/ui/action_test.go index 34a7d6694b..60733aa8e0 100644 --- a/internal/ui/action_test.go +++ b/internal/ui/action_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui_test import ( @@ -9,11 +12,11 @@ import ( ) func TestKeyActionsHints(t *testing.T) { - kk := ui.KeyActions{ + kk := ui.NewKeyActionsFromMap(ui.KeyMap{ ui.KeyF: ui.NewKeyAction("fred", nil, true), ui.KeyB: ui.NewKeyAction("blee", nil, true), ui.KeyZ: ui.NewKeyAction("zorg", nil, false), - } + }) hh := kk.Hints() diff --git a/internal/ui/app.go b/internal/ui/app.go index fd7aa2476e..d3f0353fc3 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( @@ -19,7 +22,7 @@ type App struct { Main *Pages flash *model.Flash - actions KeyActions + actions *KeyActions views map[string]tview.Primitive cmdBuff *model.FishBuff running bool @@ -30,18 +33,17 @@ type App struct { func NewApp(cfg *config.Config, context string) *App { a := App{ Application: tview.NewApplication(), - actions: make(KeyActions), - Configurator: Configurator{Config: cfg}, + actions: NewKeyActions(), + Configurator: Configurator{Config: cfg, Styles: config.NewStyles()}, Main: NewPages(), flash: model.NewFlash(model.DefaultFlashDelay), cmdBuff: model.NewFishBuff(':', model.CommandBuffer), } - a.ReloadStyles(context) a.views = map[string]tview.Primitive{ "menu": NewMenu(a.Styles), "logo": NewLogo(a.Styles), - "prompt": NewPrompt(&a, a.Config.K9s.NoIcons, a.Styles), + "prompt": NewPrompt(&a, a.Config.K9s.UI.NoIcons, a.Styles), "crumbs": NewCrumbs(a.Styles), } @@ -55,7 +57,7 @@ func (a *App) Init() { a.cmdBuff.AddListener(a) a.Styles.AddListener(a) - a.SetRoot(a.Main, true).EnableMouse(a.Config.K9s.EnableMouse) + a.SetRoot(a.Main, true).EnableMouse(a.Config.K9s.UI.EnableMouse) } // QueueUpdate queues up a ui action. @@ -131,34 +133,34 @@ func (a *App) StylesChanged(s *config.Styles) { } } -// ReloadStyles reloads skin file. -func (a *App) ReloadStyles(context string) { - a.RefreshStyles(context) -} - // Conn returns an api server connection. func (a *App) Conn() client.Connection { return a.Config.GetConnection() } func (a *App) bindKeys() { - a.actions = KeyActions{ + a.actions = NewKeyActionsFromMap(KeyMap{ KeyColon: NewKeyAction("Cmd", a.activateCmd, false), tcell.KeyCtrlR: NewKeyAction("Redraw", a.redrawCmd, false), - tcell.KeyCtrlC: NewKeyAction("Quit", a.quitCmd, false), + tcell.KeyCtrlP: NewKeyAction("Persist", a.saveCmd, false), tcell.KeyCtrlU: NewSharedKeyAction("Clear Filter", a.clearCmd, false), tcell.KeyCtrlQ: NewSharedKeyAction("Clear Filter", a.clearCmd, false), - } + }) } // BailOut exits the application. func (a *App) BailOut() { + if err := a.Config.Save(true); err != nil { + log.Error().Err(err).Msg("config save failed!") + } + a.Stop() os.Exit(0) } // ResetPrompt reset the prompt model and marks buffer as active. func (a *App) ResetPrompt(m PromptModel) { + m.ClearText(false) a.Prompt().SetModel(m) a.SetFocus(a.Prompt()) m.SetActive(true) @@ -169,6 +171,15 @@ func (a *App) ResetCmd() { a.cmdBuff.Reset() } +func (a *App) saveCmd(evt *tcell.EventKey) *tcell.EventKey { + if err := a.Config.Save(true); err != nil { + a.Flash().Err(err) + } + a.Flash().Info("current context config saved") + + return nil +} + // ActivateCmd toggle command mode. func (a *App) ActivateCmd(b bool) { a.cmdBuff.SetActive(b) @@ -189,19 +200,6 @@ func (a *App) HasCmd() bool { return a.cmdBuff.IsActive() && !a.cmdBuff.Empty() } -func (a *App) quitCmd(evt *tcell.EventKey) *tcell.EventKey { - if a.InCmdMode() { - return evt - } - - if !a.Config.K9s.NoExitOnCtrlC { - a.BailOut() - } - - // overwrite the default ctrl-c behavior of tview - return nil -} - // InCmdMode check if command mode is active. func (a *App) InCmdMode() bool { return a.Prompt().InCmdMode() @@ -209,20 +207,17 @@ func (a *App) InCmdMode() bool { // HasAction checks if key matches a registered binding. func (a *App) HasAction(key tcell.Key) (KeyAction, bool) { - act, ok := a.actions[key] - return act, ok + return a.actions.Get(key) } // GetActions returns a collection of actions. -func (a *App) GetActions() KeyActions { +func (a *App) GetActions() *KeyActions { return a.actions } // AddActions returns the application actions. -func (a *App) AddActions(aa KeyActions) { - for k, v := range aa { - a.actions[k] = v - } +func (a *App) AddActions(aa *KeyActions) { + a.actions.Merge(aa) } // Views return the application root views. diff --git a/internal/ui/app_test.go b/internal/ui/app_test.go index 133b45aa7e..2c4b137241 100644 --- a/internal/ui/app_test.go +++ b/internal/ui/app_test.go @@ -1,15 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui_test import ( "testing" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" ) func TestAppGetCmd(t *testing.T) { - a := ui.NewApp(config.NewConfig(nil), "") + a := ui.NewApp(mock.NewMockConfig(), "") a.Init() a.CmdBuff().SetText("blee", "") @@ -17,7 +20,7 @@ func TestAppGetCmd(t *testing.T) { } func TestAppInCmdMode(t *testing.T) { - a := ui.NewApp(config.NewConfig(nil), "") + a := ui.NewApp(mock.NewMockConfig(), "") a.Init() a.CmdBuff().SetText("blee", "") assert.False(t, a.InCmdMode()) @@ -27,7 +30,7 @@ func TestAppInCmdMode(t *testing.T) { } func TestAppResetCmd(t *testing.T) { - a := ui.NewApp(config.NewConfig(nil), "") + a := ui.NewApp(mock.NewMockConfig(), "") a.Init() a.CmdBuff().SetText("blee", "") @@ -37,7 +40,7 @@ func TestAppResetCmd(t *testing.T) { } func TestAppHasCmd(t *testing.T) { - a := ui.NewApp(config.NewConfig(nil), "") + a := ui.NewApp(mock.NewMockConfig(), "") a.Init() a.ActivateCmd(true) @@ -48,16 +51,16 @@ func TestAppHasCmd(t *testing.T) { } func TestAppGetActions(t *testing.T) { - a := ui.NewApp(config.NewConfig(nil), "") + a := ui.NewApp(mock.NewMockConfig(), "") a.Init() - a.AddActions(ui.KeyActions{ui.KeyZ: ui.KeyAction{Description: "zorg"}}) + a.GetActions().Add(ui.KeyZ, ui.KeyAction{Description: "zorg"}) - assert.Equal(t, 6, len(a.GetActions())) + assert.Equal(t, 6, a.GetActions().Len()) } func TestAppViews(t *testing.T) { - a := ui.NewApp(config.NewConfig(nil), "") + a := ui.NewApp(mock.NewMockConfig(), "") a.Init() vv := []string{"crumbs", "logo", "prompt", "menu"} diff --git a/internal/ui/config.go b/internal/ui/config.go index 4a95e10151..487a1cc927 100644 --- a/internal/ui/config.go +++ b/internal/ui/config.go @@ -1,20 +1,28 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( "context" "errors" - "fmt" + "io/fs" "os" "path/filepath" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/model1" + "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/render" "github.com/fsnotify/fsnotify" "github.com/rs/zerolog/log" ) // Synchronizer manages ui event queue. type synchronizer interface { + Flash() *model.Flash + Logo() *Logo + UpdateClusterInfo() QueueUpdateDraw(func()) QueueUpdate(func()) } @@ -44,16 +52,18 @@ func (c *Configurator) CustomViewsWatcher(ctx context.Context, s synchronizer) e for { select { case evt := <-w.Events: - if evt.Name == config.K9sViewConfigFile { + if evt.Name == config.AppViewsFile && evt.Op != fsnotify.Chmod { s.QueueUpdateDraw(func() { - c.RefreshCustomViews() + if err := c.RefreshCustomViews(); err != nil { + log.Warn().Err(err).Msgf("Custom views refresh failed") + } }) } case err := <-w.Errors: log.Warn().Err(err).Msg("CustomView watcher failed") return case <-ctx.Done(): - log.Debug().Msgf("CustomViewWatcher CANCELED `%s!!", config.K9sViewConfigFile) + log.Debug().Msgf("CustomViewWatcher CANCELED `%s!!", config.AppViewsFile) if err := w.Close(); err != nil { log.Error().Err(err).Msg("Closing CustomView watcher") } @@ -62,43 +72,41 @@ func (c *Configurator) CustomViewsWatcher(ctx context.Context, s synchronizer) e } }() - log.Debug().Msgf("CustomView watching `%s", config.K9sViewConfigFile) - c.RefreshCustomViews() - return w.Add(config.K9sHome()) + if err := w.Add(config.AppViewsFile); err != nil { + return err + } + + return c.RefreshCustomViews() } // RefreshCustomViews load view configuration changes. -func (c *Configurator) RefreshCustomViews() { +func (c *Configurator) RefreshCustomViews() error { if c.CustomView == nil { c.CustomView = config.NewCustomView() } else { c.CustomView.Reset() } - if err := c.CustomView.Load(config.K9sViewConfigFile); err != nil { - log.Warn().Err(err).Msgf("Custom view load failed %s", config.K9sViewConfigFile) - return - } + return c.CustomView.Load(config.AppViewsFile) } -// StylesWatcher watches for skin file changes. -func (c *Configurator) StylesWatcher(ctx context.Context, s synchronizer) error { - if !c.HasSkin() { - return nil +// SkinsDirWatcher watches for skin directory file changes. +func (c *Configurator) SkinsDirWatcher(ctx context.Context, s synchronizer) error { + if _, err := os.Stat(config.AppSkinsDir); errors.Is(err, fs.ErrNotExist) { + return err } - w, err := fsnotify.NewWatcher() if err != nil { return err } - go func() { for { select { case evt := <-w.Events: - if evt.Name == c.skinFile && evt.Op != fsnotify.Chmod { + if evt.Op != fsnotify.Chmod && filepath.Base(evt.Name) == filepath.Base(c.skinFile) { + log.Debug().Msgf("Skin changed: %s", c.skinFile) s.QueueUpdateDraw(func() { - c.RefreshStyles(c.Config.K9s.CurrentCluster) + c.RefreshStyles(s) }) } case err := <-w.Errors: @@ -114,61 +122,163 @@ func (c *Configurator) StylesWatcher(ctx context.Context, s synchronizer) error } }() - log.Debug().Msgf("SkinWatcher watching `%s", c.skinFile) - return w.Add(config.K9sHome()) + log.Debug().Msgf("SkinWatcher watching %q", config.AppSkinsDir) + return w.Add(config.AppSkinsDir) } -// BenchConfig location of the benchmarks configuration file. -func BenchConfig(context string) string { - return filepath.Join(config.K9sHome(), config.K9sBench+"-"+context+".yml") +// ConfigWatcher watches for skin settings changes. +func (c *Configurator) ConfigWatcher(ctx context.Context, s synchronizer) error { + w, err := fsnotify.NewWatcher() + if err != nil { + return err + } + + go func() { + for { + select { + case evt := <-w.Events: + if evt.Has(fsnotify.Create) || evt.Has(fsnotify.Write) { + log.Debug().Msgf("ConfigWatcher file changed: %s", evt.Name) + if evt.Name == config.AppConfigFile { + if err := c.Config.Load(evt.Name, false); err != nil { + log.Error().Err(err).Msgf("k9s config reload failed") + s.Flash().Warn("k9s config reload failed. Check k9s logs!") + s.Logo().Warn("K9s config reload failed!") + } + } else { + if err := c.Config.K9s.Reload(); err != nil { + log.Error().Err(err).Msgf("k9s context config reload failed") + s.Flash().Warn("Context config reload failed. Check k9s logs!") + s.Logo().Warn("Context config reload failed!") + } + } + s.QueueUpdateDraw(func() { + c.RefreshStyles(s) + }) + } + case err := <-w.Errors: + log.Info().Err(err).Msg("ConfigWatcher failed") + return + case <-ctx.Done(): + log.Debug().Msg("ConfigWatcher CANCELED") + if err := w.Close(); err != nil { + log.Error().Err(err).Msg("Canceling ConfigWatcher") + } + return + } + } + }() + + log.Debug().Msgf("ConfigWatcher watching: %q", config.AppConfigFile) + if err := w.Add(config.AppConfigFile); err != nil { + return err + } + + cl, ct, ok := c.activeConfig() + if !ok { + return nil + } + ctConfigFile := filepath.Join(config.AppContextConfig(cl, ct)) + log.Debug().Msgf("ConfigWatcher watching: %q", ctConfigFile) + + return w.Add(ctConfigFile) } -// RefreshStyles load for skin configuration changes. -func (c *Configurator) RefreshStyles(context string) { - c.BenchFile = BenchConfig(context) +func (c *Configurator) activeSkin() (string, bool) { + var skin string + if c.Config == nil || c.Config.K9s == nil { + return skin, false + } + + if ct, err := c.Config.K9s.ActiveContext(); err == nil && ct.Skin != "" { + if _, err := os.Stat(config.SkinFileFromName(ct.Skin)); err == nil { + skin = ct.Skin + log.Debug().Msgf("[Skin] Loading context skin (%q) from %q", skin, c.Config.K9s.ActiveContextName()) + } + } - clusterSkins := filepath.Join(config.K9sHome(), fmt.Sprintf("%s_skin.yml", context)) + if sk := c.Config.K9s.UI.Skin; skin == "" && sk != "" { + if _, err := os.Stat(config.SkinFileFromName(sk)); err == nil { + skin = sk + log.Debug().Msgf("[Skin] Loading global skin (%q)", skin) + } + } + + return skin, skin != "" +} + +func (c *Configurator) activeConfig() (cluster string, context string, ok bool) { + if c.Config == nil || c.Config.K9s == nil { + return + } + ct, err := c.Config.K9s.ActiveContext() + if err != nil { + return + } + cluster, context = ct.GetClusterName(), c.Config.K9s.ActiveContextName() + if cluster != "" && context != "" { + ok = true + } + + return +} + +// RefreshStyles load for skin configuration changes. +func (c *Configurator) RefreshStyles(s synchronizer) { + s.UpdateClusterInfo() if c.Styles == nil { c.Styles = config.NewStyles() - } else { - c.Styles.Reset() } - if err := c.Styles.Load(clusterSkins); err != nil { - if errors.Is(err, os.ErrNotExist) { - log.Warn().Msgf("No context specific skin file found -- %s", clusterSkins) - } else { - log.Error().Msgf("Failed to parse context specific skin file -- %s. %s.", clusterSkins, err) - } + defer c.loadSkinFile(s) + + cl, ct, ok := c.activeConfig() + if !ok { + return + } + // !!BOZO!! Lame move out! + if bc, err := config.EnsureBenchmarksCfgFile(cl, ct); err != nil { + log.Warn().Err(err).Msgf("No benchmark config file found: %q@%q", cl, ct) } else { - c.updateStyles(clusterSkins) + c.BenchFile = bc + } +} + +func (c *Configurator) loadSkinFile(s synchronizer) { + skin, ok := c.activeSkin() + if !ok { + log.Debug().Msgf("No custom skin found. Using stock skin") + c.updateStyles("") return } - if err := c.Styles.Load(config.K9sStylesFile); err != nil { + skinFile := config.SkinFileFromName(skin) + log.Debug().Msgf("Loading skin file: %q", skinFile) + if err := c.Styles.Load(skinFile); err != nil { if errors.Is(err, os.ErrNotExist) { - log.Warn().Msgf("No skin file found -- %s. Loading stock skins.", config.K9sStylesFile) + log.Warn().Msgf("Skin file %q not found in skins dir: %s", filepath.Base(skinFile), config.AppSkinsDir) + c.updateStyles("") } else { - log.Error().Msgf("Failed to parse skin file -- %s. %s. Loading stock skins.", config.K9sStylesFile, err) + log.Error().Msgf("Failed to parse skin file -- %s: %s.", filepath.Base(skinFile), err) + c.updateStyles(skinFile) } - c.updateStyles("") - return + } else { + c.updateStyles(skinFile) } - c.updateStyles(config.K9sStylesFile) } func (c *Configurator) updateStyles(f string) { c.skinFile = f - if !c.HasSkin() { - c.Styles.DefaultSkin() + if f == "" { + c.Styles.Reset() } c.Styles.Update() - render.ModColor = c.Styles.Frame().Status.ModifyColor.Color() - render.AddColor = c.Styles.Frame().Status.AddColor.Color() - render.ErrColor = c.Styles.Frame().Status.ErrorColor.Color() - render.StdColor = c.Styles.Frame().Status.NewColor.Color() - render.PendingColor = c.Styles.Frame().Status.PendingColor.Color() - render.HighlightColor = c.Styles.Frame().Status.HighlightColor.Color() - render.KillColor = c.Styles.Frame().Status.KillColor.Color() - render.CompletedColor = c.Styles.Frame().Status.CompletedColor.Color() + model1.ModColor = c.Styles.Frame().Status.ModifyColor.Color() + model1.AddColor = c.Styles.Frame().Status.AddColor.Color() + model1.ErrColor = c.Styles.Frame().Status.ErrorColor.Color() + model1.StdColor = c.Styles.Frame().Status.NewColor.Color() + model1.PendingColor = c.Styles.Frame().Status.PendingColor.Color() + model1.HighlightColor = c.Styles.Frame().Status.HighlightColor.Color() + model1.KillColor = c.Styles.Frame().Status.KillColor.Color() + model1.CompletedColor = c.Styles.Frame().Status.CompletedColor.Color() } diff --git a/internal/ui/config_int_test.go b/internal/ui/config_int_test.go new file mode 100644 index 0000000000..8f4c9d62eb --- /dev/null +++ b/internal/ui/config_int_test.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package ui + +import ( + "os" + "testing" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" + "github.com/stretchr/testify/assert" + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +func Test_activeConfig(t *testing.T) { + os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config") + assert.NoError(t, config.InitLocs()) + + cl, ct := "cl-1", "ct-1-1" + uu := map[string]struct { + cl, ct string + cfg *Configurator + ok bool + }{ + "empty": { + cfg: &Configurator{}, + }, + + "plain": { + cfg: &Configurator{Config: config.NewConfig( + mock.NewMockKubeSettings(&genericclioptions.ConfigFlags{ + ClusterName: &cl, + Context: &ct, + }))}, + cl: cl, + ct: ct, + ok: true, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + cfg := u.cfg + if cfg.Config != nil { + _, err := cfg.Config.K9s.ActivateContext(ct) + assert.NoError(t, err) + } + cl, ct, ok := cfg.activeConfig() + assert.Equal(t, u.ok, ok) + if ok { + assert.Equal(t, u.cl, cl) + assert.Equal(t, u.ct, ct) + } + }) + } +} diff --git a/internal/ui/config_test.go b/internal/ui/config_test.go index fd2ffe56c8..3e95694989 100644 --- a/internal/ui/config_test.go +++ b/internal/ui/config_test.go @@ -1,29 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui_test import ( "os" "path/filepath" "testing" + "time" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/config/data" + "github.com/derailed/k9s/internal/config/mock" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" "github.com/stretchr/testify/assert" + "k8s.io/cli-runtime/pkg/genericclioptions" ) +func TestSkinnedContext(t *testing.T) { + os.Setenv(config.K9sEnvConfigDir, "/tmp/k9s-test") + assert.NoError(t, config.InitLocs()) + defer assert.NoError(t, os.RemoveAll(config.K9sEnvConfigDir)) + + sf := filepath.Join("..", "config", "testdata", "skins", "black-and-wtf.yaml") + raw, err := os.ReadFile(sf) + assert.NoError(t, err) + tf := filepath.Join(config.AppSkinsDir, "black-and-wtf.yaml") + assert.NoError(t, os.WriteFile(tf, raw, data.DefaultFileMod)) + + var cfg ui.Configurator + cfg.Config = mock.NewMockConfig() + cl, ct := "cl-1", "ct-1" + flags := genericclioptions.ConfigFlags{ + ClusterName: &cl, + Context: &ct, + } + + cfg.Config.K9s = config.NewK9s( + mock.NewMockConnection(), + mock.NewMockKubeSettings(&flags)) + _, err = cfg.Config.K9s.ActivateContext("ct-1-1") + assert.NoError(t, err) + cfg.Config.K9s.UI = config.UI{Skin: "black-and-wtf"} + cfg.RefreshStyles(newMockSynchronizer()) + assert.True(t, cfg.HasSkin()) + assert.Equal(t, tcell.ColorGhostWhite.TrueColor(), model1.StdColor) + assert.Equal(t, tcell.ColorWhiteSmoke.TrueColor(), model1.ErrColor) +} + func TestBenchConfig(t *testing.T) { - os.Setenv(config.K9sConfig, "/tmp/blee") - assert.Equal(t, "/tmp/blee/bench-fred.yml", ui.BenchConfig("fred")) + os.Setenv(config.K9sEnvConfigDir, "/tmp/test-config") + assert.NoError(t, config.InitLocs()) + defer assert.NoError(t, os.RemoveAll(config.K9sEnvConfigDir)) + + bc, error := config.EnsureBenchmarksCfgFile("cl-1", "ct-1") + assert.NoError(t, error) + assert.Equal(t, "/tmp/test-config/clusters/cl-1/ct-1/benchmarks.yaml", bc) } -func TestConfiguratorRefreshStyle(t *testing.T) { - config.K9sStylesFile = filepath.Join("..", "config", "testdata", "black_and_wtf.yml") +// Helpers... - cfg := ui.Configurator{} - cfg.RefreshStyles("") +type synchronizer struct{} - assert.True(t, cfg.HasSkin()) - assert.Equal(t, tcell.ColorGhostWhite.TrueColor(), render.StdColor) - assert.Equal(t, tcell.ColorWhiteSmoke.TrueColor(), render.ErrColor) +func newMockSynchronizer() synchronizer { + return synchronizer{} +} + +func (s synchronizer) Flash() *model.Flash { + return model.NewFlash(100 * time.Millisecond) } +func (s synchronizer) Logo() *ui.Logo { return nil } +func (s synchronizer) UpdateClusterInfo() {} +func (s synchronizer) QueueUpdateDraw(func()) {} +func (s synchronizer) QueueUpdate(func()) {} diff --git a/internal/ui/crumbs.go b/internal/ui/crumbs.go index 4c976d541d..8eab72308f 100644 --- a/internal/ui/crumbs.go +++ b/internal/ui/crumbs.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( diff --git a/internal/ui/crumbs_test.go b/internal/ui/crumbs_test.go index 80b7fdb9a2..644f27459f 100644 --- a/internal/ui/crumbs_test.go +++ b/internal/ui/crumbs_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui_test import ( @@ -46,11 +49,13 @@ func (c c) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { return func (c c) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { return nil } -func (c c) SetRect(int, int, int, int) {} -func (c c) GetRect() (int, int, int, int) { return 0, 0, 0, 0 } -func (c c) GetFocusable() tview.Focusable { return c } -func (c c) Focus(func(tview.Primitive)) {} -func (c c) Blur() {} -func (c c) Start() {} -func (c c) Stop() {} -func (c c) Init(context.Context) error { return nil } +func (c c) SetRect(int, int, int, int) {} +func (c c) GetRect() (int, int, int, int) { return 0, 0, 0, 0 } +func (c c) GetFocusable() tview.Focusable { return c } +func (c c) Focus(func(tview.Primitive)) {} +func (c c) Blur() {} +func (c c) Start() {} +func (c c) Stop() {} +func (c c) Init(context.Context) error { return nil } +func (c c) SetFilter(string) {} +func (c c) SetLabelFilter(map[string]string) {} diff --git a/internal/ui/deltas.go b/internal/ui/deltas.go index 99b8c73f0e..5e4fdb7bfe 100644 --- a/internal/ui/deltas.go +++ b/internal/ui/deltas.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( diff --git a/internal/ui/deltas_test.go b/internal/ui/deltas_test.go index 6796d58911..005dc3ede0 100644 --- a/internal/ui/deltas_test.go +++ b/internal/ui/deltas_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( diff --git a/internal/ui/dialog/confirm.go b/internal/ui/dialog/confirm.go index fec298763e..461456cb4e 100644 --- a/internal/ui/dialog/confirm.go +++ b/internal/ui/dialog/confirm.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dialog import ( @@ -82,12 +85,10 @@ func ShowConfirm(styles config.Dialog, pages *ui.Pages, title, msg string, ack c cancel() }) for i := 0; i < 2; i++ { - b := f.GetButton(i) - if b == nil { - continue + if b := f.GetButton(i); b != nil { + b.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color()) + b.SetLabelColorActivated(styles.ButtonFocusFgColor.Color()) } - b.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color()) - b.SetLabelColorActivated(styles.ButtonFocusFgColor.Color()) } f.SetFocus(0) modal := tview.NewModalForm("<"+title+">", f) diff --git a/internal/ui/dialog/confirm_test.go b/internal/ui/dialog/confirm_test.go index f5cd142985..211e209188 100644 --- a/internal/ui/dialog/confirm_test.go +++ b/internal/ui/dialog/confirm_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dialog import ( diff --git a/internal/ui/dialog/delete.go b/internal/ui/dialog/delete.go index bd1bb675b3..686e95e6ac 100644 --- a/internal/ui/dialog/delete.go +++ b/internal/ui/dialog/delete.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dialog import ( diff --git a/internal/ui/dialog/delete_test.go b/internal/ui/dialog/delete_test.go index a4c141668e..39b2eba091 100644 --- a/internal/ui/dialog/delete_test.go +++ b/internal/ui/dialog/delete_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dialog import ( diff --git a/internal/ui/dialog/error.go b/internal/ui/dialog/error.go index f39e31fd36..1d81db5e77 100644 --- a/internal/ui/dialog/error.go +++ b/internal/ui/dialog/error.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dialog import ( @@ -38,7 +41,7 @@ func ShowError(styles config.Dialog, pages *ui.Pages, msg string) { } func cowTalk(says string) string { - msg := fmt.Sprintf("< Ruroh? %s >", says) + msg := fmt.Sprintf("< Ruroh? %s >", strings.TrimSuffix(says, "\n")) buff := make([]string, 0, len(cow)+3) buff = append(buff, msg) buff = append(buff, cow...) diff --git a/internal/ui/dialog/error_test.go b/internal/ui/dialog/error_test.go index f882ac68da..b21fa4c67b 100644 --- a/internal/ui/dialog/error_test.go +++ b/internal/ui/dialog/error_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dialog import ( diff --git a/internal/ui/dialog/prompt.go b/internal/ui/dialog/prompt.go new file mode 100644 index 0000000000..8232d2d215 --- /dev/null +++ b/internal/ui/dialog/prompt.go @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package dialog + +import ( + "context" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tview" +) + +type promptAction func(ctx context.Context) + +// ShowPrompt pops a prompt dialog. +func ShowPrompt(styles config.Dialog, pages *ui.Pages, title, msg string, action promptAction, cancel cancelFunc) { + f := tview.NewForm() + f.SetItemPadding(0) + f.SetButtonsAlign(tview.AlignCenter). + SetButtonBackgroundColor(styles.ButtonBgColor.Color()). + SetButtonTextColor(styles.ButtonFgColor.Color()). + SetLabelColor(styles.LabelFgColor.Color()). + SetFieldTextColor(styles.FieldFgColor.Color()) + + ctx, cancelCtx := context.WithCancel(context.Background()) + + f.AddButton("Cancel", func() { + dismiss(pages) + cancelCtx() + cancel() + }) + + for i := 0; i < f.GetButtonCount(); i++ { + b := f.GetButton(i) + if b == nil { + continue + } + b.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color()) + b.SetLabelColorActivated(styles.ButtonFocusFgColor.Color()) + } + + f.SetFocus(0) + modal := tview.NewModalForm("<"+title+">", f) + modal.SetText(msg) + modal.SetTextColor(styles.FgColor.Color()) + modal.SetDoneFunc(func(int, string) { + dismiss(pages) + cancelCtx() + cancel() + }) + + pages.AddPage(dialogKey, modal, false, false) + pages.ShowPage(dialogKey) + + go func() { + action(ctx) + dismiss(pages) + }() +} diff --git a/internal/ui/dialog/prompt_test.go b/internal/ui/dialog/prompt_test.go new file mode 100644 index 0000000000..a5a7c0229a --- /dev/null +++ b/internal/ui/dialog/prompt_test.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package dialog + +import ( + "context" + "testing" + "time" + + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tcell/v2" + "github.com/derailed/tview" + "github.com/stretchr/testify/assert" +) + +func TestShowPrompt(t *testing.T) { + t.Run("waiting done", func(t *testing.T) { + a := tview.NewApplication() + p := ui.NewPages() + a.SetRoot(p, false) + + ShowPrompt(config.Dialog{}, p, "Running", "Pod", func(context.Context) { + time.Sleep(time.Millisecond) + }, func() { + t.Errorf("unexpected cancellations") + }) + }) + + t.Run("canceled", func(t *testing.T) { + a := tview.NewApplication() + p := ui.NewPages() + a.SetRoot(p, false) + + go ShowPrompt(config.Dialog{}, p, "Running", "Pod", func(ctx context.Context) { + select { + case <-time.After(time.Second): + t.Errorf("expected cancellations") + case <-ctx.Done(): + } + }, func() {}) + + time.Sleep(time.Second / 2) + d := p.GetPrimitive(dialogKey).(*tview.ModalForm) + if assert.NotNil(t, d) { + d.InputHandler()(tcell.NewEventKey(tcell.KeyEnter, '\n', 0), func(tview.Primitive) {}) + } + }) +} diff --git a/internal/ui/dialog/selection.go b/internal/ui/dialog/selection.go new file mode 100644 index 0000000000..fc6e545cca --- /dev/null +++ b/internal/ui/dialog/selection.go @@ -0,0 +1,30 @@ +package dialog + +import ( + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tview" +) + +type SelectAction func(index int) + +func ShowSelection(styles config.Dialog, pages *ui.Pages, title string, options []string, action SelectAction) { + list := tview.NewList() + list.ShowSecondaryText(false) + list.SetSelectedTextColor(styles.ButtonFocusFgColor.Color()) + list.SetSelectedBackgroundColor(styles.ButtonFocusBgColor.Color()) + + for _, option := range options { + list.AddItem(option, "", 0, nil) + list.AddItem(option, "", 0, nil) + } + + modal := ui.NewModalList("<"+title+">", list) + modal.SetDoneFunc(func(i int, s string) { + dismiss(pages) + action(i) + }) + + pages.AddPage(dialogKey, modal, false, false) + pages.ShowPage(dialogKey) +} diff --git a/internal/ui/dialog/transfer.go b/internal/ui/dialog/transfer.go index 7d51cba408..3141a8943c 100644 --- a/internal/ui/dialog/transfer.go +++ b/internal/ui/dialog/transfer.go @@ -1,6 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package dialog import ( + "strconv" "strings" "github.com/derailed/k9s/internal/config" @@ -10,12 +14,19 @@ import ( const confirmKey = "confirm" -type TransferFn func(from, to, co string, download, no_preserve bool) bool +type TransferFn func(TransferArgs) bool + +type TransferArgs struct { + From, To, CO string + Download, NoPreserve bool + Retries int +} type TransferDialogOpts struct { Containers []string Pod string Title, Message string + Retries int Ack TransferFn Cancel cancelFunc } @@ -35,44 +46,53 @@ func ShowUploads(styles config.Dialog, pages *ui.Pages, opts TransferDialogOpts) modal := tview.NewModalForm("<"+opts.Title+">", f) - from, to := opts.Pod, "" + args := TransferArgs{ + From: opts.Pod, + Retries: opts.Retries, + } var fromField, toField *tview.InputField - download := true - f.AddCheckbox("Download:", download, func(_ string, flag bool) { + args.Download = true + f.AddCheckbox("Download:", args.Download, func(_ string, flag bool) { if flag { modal.SetText(strings.Replace(opts.Message, "Upload", "Download", 1)) } else { modal.SetText(strings.Replace(opts.Message, "Download", "Upload", 1)) } - download = flag - from, to = to, from - fromField.SetText(from) - toField.SetText(to) + args.Download = flag + args.From, args.To = args.To, args.From + fromField.SetText(args.From) + toField.SetText(args.To) }) - f.AddInputField("From:", from, 40, nil, func(t string) { - from = t + f.AddInputField("From:", args.From, 40, nil, func(v string) { + args.From = v }) - f.AddInputField("To:", to, 40, nil, func(t string) { - to = t + f.AddInputField("To:", args.To, 40, nil, func(v string) { + args.To = v }) fromField, _ = f.GetFormItemByLabel("From:").(*tview.InputField) toField, _ = f.GetFormItemByLabel("To:").(*tview.InputField) - var no_preserve bool - f.AddCheckbox("NoPreserve:", no_preserve, func(_ string, f bool) { - no_preserve = f + f.AddCheckbox("NoPreserve:", args.NoPreserve, func(_ string, f bool) { + args.NoPreserve = f }) - var co string if len(opts.Containers) > 0 { - co = opts.Containers[0] + args.CO = opts.Containers[0] } - f.AddInputField("Container:", co, 30, nil, func(t string) { - co = t + f.AddInputField("Container:", args.CO, 30, nil, func(v string) { + args.CO = v + }) + retries := strconv.Itoa(opts.Retries) + f.AddInputField("Retries:", retries, 30, nil, func(v string) { + retries = v + + if retriesInt, err := strconv.Atoi(retries); err == nil { + args.Retries = retriesInt + } }) f.AddButton("OK", func() { - if !opts.Ack(from, to, co, download, no_preserve) { + if !opts.Ack(args) { return } dismissConfirm(pages) diff --git a/internal/ui/flash.go b/internal/ui/flash.go index 7455a1321f..f188464193 100644 --- a/internal/ui/flash.go +++ b/internal/ui/flash.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( @@ -82,7 +85,7 @@ func (f *Flash) SetMessage(m model.LevelMessage) { } func (f *Flash) flashEmoji(l model.FlashLevel) string { - if f.app.Config.K9s.NoIcons { + if f.app.Config.K9s.UI.NoIcons { return "" } // nolint:exhaustive diff --git a/internal/ui/flash_test.go b/internal/ui/flash_test.go index 76acec05d8..5122152efc 100644 --- a/internal/ui/flash_test.go +++ b/internal/ui/flash_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui_test import ( @@ -5,7 +8,7 @@ import ( "testing" "time" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" @@ -22,7 +25,7 @@ func TestFlash(t *testing.T) { "err": {l: model.FlashErr, i: "hello", e: "😡 hello\n"}, } - a := ui.NewApp(config.NewConfig(nil), "test") + a := ui.NewApp(mock.NewMockConfig(), "test") f := ui.NewFlash(a) f.SetTestMode(true) ctx, cancel := context.WithCancel(context.Background()) diff --git a/internal/ui/indicator.go b/internal/ui/indicator.go index cdcabdb751..ed4645a424 100644 --- a/internal/ui/indicator.go +++ b/internal/ui/indicator.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( @@ -53,8 +56,8 @@ func (s *StatusIndicator) ClusterInfoUpdated(data model.ClusterMeta) { s.SetPermanent(fmt.Sprintf( statusIndicatorFmt, data.K9sVer, + data.Context, data.Cluster, - data.User, data.K8sVer, render.PrintPerc(data.Cpu), render.PrintPerc(data.Mem), @@ -71,8 +74,8 @@ func (s *StatusIndicator) ClusterInfoChanged(prev, cur model.ClusterMeta) { s.SetPermanent(fmt.Sprintf( statusIndicatorFmt, cur.K9sVer, + cur.Context, cur.Cluster, - cur.User, cur.K8sVer, AsPercDelta(prev.Cpu, cur.Cpu), AsPercDelta(prev.Cpu, cur.Mem), diff --git a/internal/ui/indicator_test.go b/internal/ui/indicator_test.go index 7a3a09226d..40032517e6 100644 --- a/internal/ui/indicator_test.go +++ b/internal/ui/indicator_test.go @@ -1,15 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui_test import ( "testing" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" ) func TestIndicatorReset(t *testing.T) { - i := ui.NewStatusIndicator(ui.NewApp(config.NewConfig(nil), ""), config.NewStyles()) + i := ui.NewStatusIndicator(ui.NewApp(mock.NewMockConfig(), ""), config.NewStyles()) i.SetPermanent("Blee") i.Info("duh") i.Reset() @@ -18,21 +22,21 @@ func TestIndicatorReset(t *testing.T) { } func TestIndicatorInfo(t *testing.T) { - i := ui.NewStatusIndicator(ui.NewApp(config.NewConfig(nil), ""), config.NewStyles()) + i := ui.NewStatusIndicator(ui.NewApp(mock.NewMockConfig(), ""), config.NewStyles()) i.Info("Blee") assert.Equal(t, "[lawngreen::b] \n", i.GetText(false)) } func TestIndicatorWarn(t *testing.T) { - i := ui.NewStatusIndicator(ui.NewApp(config.NewConfig(nil), ""), config.NewStyles()) + i := ui.NewStatusIndicator(ui.NewApp(mock.NewMockConfig(), ""), config.NewStyles()) i.Warn("Blee") assert.Equal(t, "[mediumvioletred::b] \n", i.GetText(false)) } func TestIndicatorErr(t *testing.T) { - i := ui.NewStatusIndicator(ui.NewApp(config.NewConfig(nil), ""), config.NewStyles()) + i := ui.NewStatusIndicator(ui.NewApp(mock.NewMockConfig(), ""), config.NewStyles()) i.Err("Blee") assert.Equal(t, "[orangered::b] \n", i.GetText(false)) diff --git a/internal/ui/key.go b/internal/ui/key.go index fbf0028c0e..ff90b310ba 100644 --- a/internal/ui/key.go +++ b/internal/ui/key.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import "github.com/derailed/tcell/v2" diff --git a/internal/ui/logo.go b/internal/ui/logo.go index a8cddad04c..34724f46ab 100644 --- a/internal/ui/logo.go +++ b/internal/ui/logo.go @@ -1,8 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( "fmt" "strings" + "sync" "github.com/derailed/k9s/internal/config" "github.com/derailed/tview" @@ -14,6 +18,7 @@ type Logo struct { logo, status *tview.TextView styles *config.Styles + mx sync.Mutex } // NewLogo returns a new logo. @@ -47,7 +52,10 @@ func (l *Logo) Status() *tview.TextView { // StylesChanged notifies the skin changed. func (l *Logo) StylesChanged(s *config.Styles) { l.styles = s - l.Reset() + l.SetBackgroundColor(l.styles.BgColor()) + l.status.SetBackgroundColor(l.styles.BgColor()) + l.logo.SetBackgroundColor(l.styles.BgColor()) + l.refreshLogo(l.styles.Body().LogoColor) } // IsBenchmarking checks if benchmarking is active or not. @@ -59,10 +67,7 @@ func (l *Logo) IsBenchmarking() bool { // Reset clears out the logo view and resets colors. func (l *Logo) Reset() { l.status.Clear() - l.SetBackgroundColor(l.styles.BgColor()) - l.status.SetBackgroundColor(l.styles.BgColor()) - l.logo.SetBackgroundColor(l.styles.BgColor()) - l.refreshLogo(l.styles.Body().LogoColor) + l.StylesChanged(l.styles) } // Err displays a log error state. @@ -86,6 +91,9 @@ func (l *Logo) update(msg string, c config.Color) { } func (l *Logo) refreshStatus(msg string, c config.Color) { + l.mx.Lock() + defer l.mx.Unlock() + l.status.SetBackgroundColor(c.Color()) l.status.SetText( fmt.Sprintf("[%s::b]%s", l.styles.Body().LogoColorMsg, msg), @@ -93,6 +101,8 @@ func (l *Logo) refreshStatus(msg string, c config.Color) { } func (l *Logo) refreshLogo(c config.Color) { + l.mx.Lock() + defer l.mx.Unlock() l.logo.Clear() for i, s := range LogoSmall { fmt.Fprintf(l.logo, "[%s::b]%s", c, s) diff --git a/internal/ui/logo_test.go b/internal/ui/logo_test.go index 2e3c053c69..6a2fc0a0d4 100644 --- a/internal/ui/logo_test.go +++ b/internal/ui/logo_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui_test import ( diff --git a/internal/ui/menu.go b/internal/ui/menu.go index 02cbedbe79..f86a81e919 100644 --- a/internal/ui/menu.go +++ b/internal/ui/menu.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( diff --git a/internal/ui/menu_test.go b/internal/ui/menu_test.go index ab000d93d0..9a4788035c 100644 --- a/internal/ui/menu_test.go +++ b/internal/ui/menu_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui_test import ( @@ -24,16 +27,16 @@ func TestNewMenu(t *testing.T) { func TestActionHints(t *testing.T) { uu := map[string]struct { - aa ui.KeyActions + aa *ui.KeyActions e model.MenuHints }{ "a": { - aa: ui.KeyActions{ + aa: ui.NewKeyActionsFromMap(ui.KeyMap{ ui.KeyB: ui.NewKeyAction("bleeB", nil, true), ui.KeyA: ui.NewKeyAction("bleeA", nil, true), ui.Key0: ui.NewKeyAction("zero", nil, true), ui.Key1: ui.NewKeyAction("one", nil, false), - }, + }), e: model.MenuHints{ {Mnemonic: "0", Description: "zero", Visible: true}, {Mnemonic: "1", Description: "one", Visible: false}, diff --git a/internal/ui/modal_list.go b/internal/ui/modal_list.go new file mode 100644 index 0000000000..1827561a36 --- /dev/null +++ b/internal/ui/modal_list.go @@ -0,0 +1,109 @@ +package ui + +import ( + "github.com/derailed/tcell/v2" + "github.com/derailed/tview" +) + +type ModalList struct { + *tview.Box + + // The list embedded in the modal's frame. + list *tview.List + + // The frame embedded in the modal. + frame *tview.Frame + + // The optional callback for when the user clicked one of the items. It + // receives the index of the clicked item and the item's text. + done func(int, string) +} + +func NewModalList(title string, list *tview.List) *ModalList { + m := &ModalList{Box: tview.NewBox()} + + m.list = list + m.list.SetBackgroundColor(tview.Styles.ContrastBackgroundColor).SetBorderPadding(0, 0, 0, 0) + m.list.SetSelectedFunc(func(i int, main string, _ string, _ rune) { + if m.done != nil { + m.done(i, main) + } + }) + m.list.SetDoneFunc(func() { + if m.done != nil { + m.done(-1, "") + } + }) + + m.frame = tview.NewFrame(m.list).SetBorders(0, 0, 1, 0, 0, 0) + m.frame.SetBorder(true). + SetBackgroundColor(tview.Styles.ContrastBackgroundColor). + SetBorderPadding(1, 1, 1, 1) + m.frame.SetTitle(title) + m.frame.SetTitleColor(tcell.ColorAqua) + + return m +} + +// Draw draws this primitive onto the screen. +func (m *ModalList) Draw(screen tcell.Screen) { + // Calculate the width of this modal. + width := 0 + for i := 0; i < m.list.GetItemCount(); i++ { + main, secondary := m.list.GetItemText(i) + width = max(width, len(main)+len(secondary)+2) + } + + screenWidth, screenHeight := screen.Size() + + // Set the modal's position and size. + height := m.list.GetItemCount() + 4 + width += 2 + x := (screenWidth - width) / 2 + y := (screenHeight - height) / 2 + m.SetRect(x, y, width, height) + + // Draw the frame. + m.frame.SetRect(x, y, width, height) + m.frame.Draw(screen) +} + +func (m *ModalList) SetDoneFunc(handler func(int, string)) *ModalList { + m.done = handler + return m +} + +// Focus is called when this primitive receives focus. +func (m *ModalList) Focus(delegate func(p tview.Primitive)) { + delegate(m.list) +} + +// HasFocus returns whether this primitive has focus. +func (m *ModalList) HasFocus() bool { + return m.list.HasFocus() +} + +// MouseHandler returns the mouse handler for this primitive. +func (m *ModalList) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { + return m.WrapMouseHandler(func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (consumed bool, capture tview.Primitive) { + // Pass mouse events on to the form. + consumed, capture = m.list.MouseHandler()(action, event, setFocus) + if !consumed && action == tview.MouseLeftClick && m.InRect(event.Position()) { + setFocus(m) + consumed = true + } + return + }) +} + +// InputHandler returns the handler for this primitive. +func (m *ModalList) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + return m.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + if m.frame.HasFocus() { + if handler := m.frame.InputHandler(); handler != nil { + handler(event, setFocus) + return + } + } + }) +} diff --git a/internal/ui/padding.go b/internal/ui/padding.go index 62272da4b7..25cacdc541 100644 --- a/internal/ui/padding.go +++ b/internal/ui/padding.go @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( "strings" "unicode" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" ) @@ -11,26 +15,27 @@ import ( type MaxyPad []int // ComputeMaxColumns figures out column max size and necessary padding. -func ComputeMaxColumns(pads MaxyPad, sortColName string, header render.Header, ee render.RowEvents) { +func ComputeMaxColumns(pads MaxyPad, sortColName string, t *model1.TableData) { const colPadding = 1 - for index, h := range header { - pads[index] = len(h.Name) - if h.Name == sortColName { - pads[index] = len(h.Name) + 2 + for i, n := range t.ColumnNames(true) { + pads[i] = len(n) + if n == sortColName { + pads[i] += 2 } } var row int - for _, e := range ee { - for index, field := range e.Row.Fields { + t.RowsRange(func(_ int, re model1.RowEvent) bool { + for index, field := range re.Row.Fields { width := len(field) + colPadding if index < len(pads) && width > pads[index] { pads[index] = width } } row++ - } + return true + }) } // IsASCII checks if table cell has all ascii characters. diff --git a/internal/ui/padding_test.go b/internal/ui/padding_test.go index 18aa83e54d..0dbcb87a38 100644 --- a/internal/ui/padding_test.go +++ b/internal/ui/padding_test.go @@ -1,72 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( "testing" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model1" "github.com/stretchr/testify/assert" ) func TestMaxColumn(t *testing.T) { uu := map[string]struct { - t *render.TableData + t *model1.TableData s string e MaxyPad }{ "ascii col 0": { - &render.TableData{ - Header: render.Header{render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "B"}}, - RowEvents: render.RowEvents{ - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"hello", "world"}, + model1.NewTableDataWithRows( + client.NewGVR("test"), + model1.Header{model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "B"}}, + model1.NewRowEventsWithEvts( + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"hello", "world"}, }, }, - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"yo", "mama"}, + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"yo", "mama"}, }, }, - }, - }, + ), + ), "A", MaxyPad{6, 6}, }, "ascii col 1": { - &render.TableData{ - Header: render.Header{render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "B"}}, - RowEvents: render.RowEvents{ - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"hello", "world"}, + model1.NewTableDataWithRows( + client.NewGVR("test"), + model1.Header{model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "B"}}, + model1.NewRowEventsWithEvts( + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"hello", "world"}, }, }, - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"yo", "mama"}, + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"yo", "mama"}, }, }, - }, - }, + ), + ), "B", MaxyPad{6, 6}, }, "non_ascii": { - &render.TableData{ - Header: render.Header{render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "B"}}, - RowEvents: render.RowEvents{ - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"Hello World lord of ipsums 😅", "world"}, + model1.NewTableDataWithRows( + client.NewGVR("test"), + model1.Header{model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "B"}}, + model1.NewRowEventsWithEvts( + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"Hello World lord of ipsums 😅", "world"}, }, }, - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"o", "mama"}, + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"o", "mama"}, }, }, - }, - }, + ), + ), "A", MaxyPad{32, 6}, }, @@ -75,8 +82,8 @@ func TestMaxColumn(t *testing.T) { for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - pads := make(MaxyPad, len(u.t.Header)) - ComputeMaxColumns(pads, u.s, u.t.Header, u.t.RowEvents) + pads := make(MaxyPad, u.t.HeaderCount()) + ComputeMaxColumns(pads, u.s, u.t) assert.Equal(t, u.e, pads) }) } @@ -116,27 +123,28 @@ func TestPad(t *testing.T) { } func BenchmarkMaxColumn(b *testing.B) { - table := render.TableData{ - Header: render.Header{render.HeaderColumn{Name: "A"}, render.HeaderColumn{Name: "B"}}, - RowEvents: render.RowEvents{ - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"hello", "world"}, + table := model1.NewTableDataWithRows( + client.NewGVR("test"), + model1.Header{model1.HeaderColumn{Name: "A"}, model1.HeaderColumn{Name: "B"}}, + model1.NewRowEventsWithEvts( + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"hello", "world"}, }, }, - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"yo", "mama"}, + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"yo", "mama"}, }, }, - }, - } + ), + ) - pads := make(MaxyPad, len(table.Header)) + pads := make(MaxyPad, table.HeaderCount()) b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { - ComputeMaxColumns(pads, "A", table.Header, table.RowEvents) + ComputeMaxColumns(pads, "A", table) } } diff --git a/internal/ui/pages.go b/internal/ui/pages.go index 2f0835ef8e..b5864e3bec 100644 --- a/internal/ui/pages.go +++ b/internal/ui/pages.go @@ -1,8 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( "fmt" - "github.com/derailed/k9s/internal/model" "github.com/derailed/tview" "github.com/rs/zerolog/log" @@ -29,7 +31,7 @@ func NewPages() *Pages { func (p *Pages) IsTopDialog() bool { _, pa := p.GetFrontPage() switch pa.(type) { - case *tview.ModalForm: + case *tview.ModalForm, *ModalList: return true default: return false @@ -99,7 +101,7 @@ func (p *Pages) StackTop(top model.Component) { func componentID(c model.Component) string { if c.Name() == "" { - panic("Component has no name") + log.Error().Msg("Component has no name") } return fmt.Sprintf("%s-%p", c.Name(), c) } diff --git a/internal/ui/pages_test.go b/internal/ui/pages_test.go index f6e447e497..2dc0ea7c28 100644 --- a/internal/ui/pages_test.go +++ b/internal/ui/pages_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui_test import ( diff --git a/internal/ui/prompt.go b/internal/ui/prompt.go index 117dd10017..45613a4830 100644 --- a/internal/ui/prompt.go +++ b/internal/ui/prompt.go @@ -1,7 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( "fmt" + "sync" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/model" @@ -80,6 +84,7 @@ type Prompt struct { styles *config.Styles model PromptModel spacer int + mx sync.RWMutex } // NewPrompt returns a new command view. @@ -119,6 +124,14 @@ func (p *Prompt) SendStrokes(s string) { } } +// Deactivate sets the prompt as inactive. +func (p *Prompt) Deactivate() { + if p.model != nil { + p.model.ClearText(true) + p.model.SetActive(false) + } +} + // SetModel sets the prompt buffer model. func (p *Prompt) SetModel(m PromptModel) { if p.model != nil { @@ -138,24 +151,31 @@ func (p *Prompt) keyboard(evt *tcell.EventKey) *tcell.EventKey { switch evt.Key() { case tcell.KeyBackspace2, tcell.KeyBackspace, tcell.KeyDelete: p.model.Delete() + case tcell.KeyRune: p.model.Add(evt.Rune()) + case tcell.KeyEscape: p.model.ClearText(true) p.model.SetActive(false) + case tcell.KeyEnter, tcell.KeyCtrlE: p.model.SetText(p.model.GetText(), "") p.model.SetActive(false) + case tcell.KeyCtrlW, tcell.KeyCtrlU: p.model.ClearText(true) + case tcell.KeyUp: if s, ok := m.NextSuggestion(); ok { - p.suggest(p.model.GetText(), s) + p.model.SetText(p.model.GetText(), s) } + case tcell.KeyDown: if s, ok := m.PrevSuggestion(); ok { - p.suggest(p.model.GetText(), s) + p.model.SetText(p.model.GetText(), s) } + case tcell.KeyTab, tcell.KeyRight, tcell.KeyCtrlF: if s, ok := m.CurrentSuggestion(); ok { p.model.SetText(p.model.GetText()+s, "") @@ -188,17 +208,29 @@ func (p *Prompt) activate() { p.model.Notify(false) } -func (p *Prompt) update(text, suggestion string) { - p.Clear() - p.write(text, suggestion) +func (p *Prompt) Clear() { + p.mx.Lock() + defer p.mx.Unlock() + + p.TextView.Clear() } -func (p *Prompt) suggest(text, suggestion string) { +func (p *Prompt) Draw(sc tcell.Screen) { + p.mx.RLock() + defer p.mx.RUnlock() + + p.TextView.Draw(sc) +} + +func (p *Prompt) update(text, suggestion string) { p.Clear() p.write(text, suggestion) } func (p *Prompt) write(text, suggest string) { + p.mx.Lock() + defer p.mx.Unlock() + p.SetCursorIndex(p.spacer + len(text)) txt := text if suggest != "" { @@ -222,7 +254,7 @@ func (p *Prompt) BufferChanged(text, suggestion string) { // SuggestionChanged notifies the suggestion changed. func (p *Prompt) SuggestionChanged(text, suggestion string) { - p.suggest(text, suggestion) + p.update(text, suggestion) } // BufferActive indicates the buff activity changed. diff --git a/internal/ui/prompt_test.go b/internal/ui/prompt_test.go index 41b1c96497..56a6e25025 100644 --- a/internal/ui/prompt_test.go +++ b/internal/ui/prompt_test.go @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui_test import ( - "github.com/derailed/tcell/v2" "testing" + "github.com/derailed/tcell/v2" + "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/ui" @@ -47,13 +51,29 @@ func TestCmdMode(t *testing.T) { } } +func TestPrompt_Deactivate(t *testing.T) { + model := model.NewFishBuff(':', model.CommandBuffer) + v := ui.NewPrompt(&ui.App{}, true, config.NewStyles()) + v.SetModel(model) + model.AddListener(v) + + model.SetActive(true) + if assert.True(t, v.InCmdMode()) { + v.Deactivate() + assert.False(t, v.InCmdMode()) + } +} + // Tests that, when active, the prompt has the appropriate color func TestPromptColor(t *testing.T) { styles := config.NewStyles() app := ui.App{} // Make sure to have different values to be sure that the prompt color actually changes depending on its type - assert.NotEqual(t, styles.Prompt().Border.DefaultColor.Color(), styles.Prompt().Border.CommandColor.Color()) + assert.NotEqual(t, + styles.Prompt().Border.DefaultColor.Color(), + styles.Prompt().Border.CommandColor.Color(), + ) testCases := []struct { kind model.BufferKind diff --git a/internal/ui/select_table.go b/internal/ui/select_table.go index 335f362209..155ef371d2 100644 --- a/internal/ui/select_table.go +++ b/internal/ui/select_table.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( @@ -12,7 +15,8 @@ type SelectTable struct { model Tabular selectedFn func(string) string marks map[string]struct{} - fgColor tcell.Color + selFgColor tcell.Color + selBgColor tcell.Color } // SetModel sets the table model. @@ -55,7 +59,7 @@ func (s *SelectTable) GetSelectedItems() []string { return items } -// GetRowID returns the row id at at given location. +// GetRowID returns the row id at given location. func (s *SelectTable) GetRowID(index int) (string, bool) { cell := s.GetCell(index, 0) if cell == nil { @@ -99,21 +103,21 @@ func (s *SelectTable) GetSelectedRowIndex() int { } // SelectRow select a given row by index. -func (s *SelectTable) SelectRow(r int, broadcast bool) { +func (s *SelectTable) SelectRow(r, c int, broadcast bool) { if !broadcast { s.SetSelectionChangedFunc(nil) } - if c := s.model.Count(); c > 0 && r-1 > c { + if c := s.model.RowCount(); c > 0 && r-1 > c { r = c + 1 } defer s.SetSelectionChangedFunc(s.selectionChanged) - s.Select(r, 0) + s.Select(r, c) } // UpdateSelection refresh selected row. func (s *SelectTable) updateSelection(broadcast bool) { - r, _ := s.GetSelection() - s.SelectRow(r, broadcast) + r, c := s.GetSelection() + s.SelectRow(r, c, broadcast) } func (s *SelectTable) selectionChanged(r, c int) { @@ -121,7 +125,9 @@ func (s *SelectTable) selectionChanged(r, c int) { return } if cell := s.GetCell(r, c); cell != nil { - s.SetSelectedStyle(tcell.StyleDefault.Foreground(s.fgColor).Background(cell.Color).Attributes(tcell.AttrBold)) + s.SetSelectedStyle( + tcell.StyleDefault.Foreground(s.selFgColor). + Background(cell.Color).Attributes(tcell.AttrBold)) } } @@ -212,7 +218,7 @@ func (s *SelectTable) markRange(prev, curr int) { } // IsMarked returns true if this item was marked. -func (s *Table) IsMarked(item string) bool { +func (s *SelectTable) IsMarked(item string) bool { _, ok := s.marks[item] return ok } diff --git a/internal/ui/splash.go b/internal/ui/splash.go index ba44f2091e..bfe58e461b 100644 --- a/internal/ui/splash.go +++ b/internal/ui/splash.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( diff --git a/internal/ui/splash_test.go b/internal/ui/splash_test.go index 2113819c92..69b4b50d4c 100644 --- a/internal/ui/splash_test.go +++ b/internal/ui/splash_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui_test import ( diff --git a/internal/ui/table.go b/internal/ui/table.go index 08d57436e9..a1cd7365ef 100644 --- a/internal/ui/table.go +++ b/internal/ui/table.go @@ -1,16 +1,20 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( "context" - "errors" "fmt" - "strings" + "sync" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/vul" "github.com/derailed/tcell/v2" "github.com/derailed/tview" "github.com/rs/zerolog/log" @@ -18,12 +22,14 @@ import ( "golang.org/x/text/language" ) +const maxTruncate = 50 + type ( // ColorerFunc represents a row colorer. - ColorerFunc func(ns string, evt render.RowEvent) tcell.Color + ColorerFunc func(ns string, evt model1.RowEvent) tcell.Color // DecorateFunc represents a row decorator. - DecorateFunc func(*render.TableData) + DecorateFunc func(*model1.TableData) // SelectedRowFunc a table selection callback. SelectedRowFunc func(r int) @@ -32,21 +38,22 @@ type ( // Table represents tabular data. type Table struct { gvr client.GVR - sortCol SortColumn + sortCol model1.SortColumn manualSort bool - header render.Header Path string Extras string *SelectTable - actions KeyActions + actions *KeyActions cmdBuff *model.FishBuff styles *config.Styles viewSetting *config.ViewSetting - colorerFn render.ColorerFunc + colorerFn model1.ColorerFunc decorateFn DecorateFunc wide bool toast bool hasMetrics bool + ctx context.Context + mx sync.RWMutex } // NewTable returns a new table view. @@ -58,12 +65,74 @@ func NewTable(gvr client.GVR) *Table { marks: make(map[string]struct{}), }, gvr: gvr, - actions: make(KeyActions), + actions: NewKeyActions(), cmdBuff: model.NewFishBuff('/', model.FilterBuffer), - sortCol: SortColumn{asc: true}, + sortCol: model1.SortColumn{ASC: true}, } } +func (t *Table) setSortCol(sc model1.SortColumn) { + t.mx.Lock() + defer t.mx.Unlock() + + t.sortCol = sc +} + +func (t *Table) toggleSortCol() { + t.mx.Lock() + defer t.mx.Unlock() + + t.sortCol.ASC = !t.sortCol.ASC +} + +func (t *Table) getSortCol() model1.SortColumn { + t.mx.RLock() + defer t.mx.RUnlock() + + return t.sortCol +} + +func (t *Table) setMSort(b bool) { + t.mx.Lock() + defer t.mx.Unlock() + + t.manualSort = b +} + +func (t *Table) getMSort() bool { + t.mx.RLock() + defer t.mx.RUnlock() + + return t.manualSort +} + +func (t *Table) setVs(vs *config.ViewSetting) bool { + t.mx.Lock() + defer t.mx.Unlock() + + if !t.viewSetting.Equals(vs) { + t.viewSetting = vs + return true + } + + return false +} + +func (t *Table) getVs() *config.ViewSetting { + t.mx.RLock() + defer t.mx.RUnlock() + + return t.viewSetting +} + +func (t *Table) GetContext() context.Context { + return t.ctx +} + +func (t *Table) SetContext(ctx context.Context) { + t.ctx = ctx +} + // Init initializes the component. func (t *Table) Init(ctx context.Context) { t.SetFixed(1, 0) @@ -85,9 +154,11 @@ func (t *Table) Init(ctx context.Context) { func (t *Table) GVR() client.GVR { return t.gvr } // ViewSettingsChanged notifies listener the view configuration changed. -func (t *Table) ViewSettingsChanged(settings config.ViewSetting) { - t.viewSetting, t.manualSort = &settings, false - t.Refresh() +func (t *Table) ViewSettingsChanged(vs config.ViewSetting) { + if t.setVs(&vs) { + t.setMSort(false) + t.Refresh() + } } // StylesChanged notifies the skin changed. @@ -95,8 +166,11 @@ func (t *Table) StylesChanged(s *config.Styles) { t.SetBackgroundColor(s.Table().BgColor.Color()) t.SetBorderColor(s.Frame().Border.FgColor.Color()) t.SetBorderFocusColor(s.Frame().Border.FocusColor.Color()) - t.SetSelectedStyle(tcell.StyleDefault.Foreground(t.styles.Table().CursorFgColor.Color()).Background(t.styles.Table().CursorBgColor.Color()).Attributes(tcell.AttrBold)) - t.fgColor = s.Table().CursorFgColor.Color() + t.SetSelectedStyle( + tcell.StyleDefault.Foreground(t.styles.Table().CursorFgColor.Color()). + Background(t.styles.Table().CursorBgColor.Color()).Attributes(tcell.AttrBold)) + t.selFgColor = s.Table().CursorFgColor.Color() + t.selBgColor = s.Table().CursorBgColor.Color() t.Refresh() } @@ -119,7 +193,7 @@ func (t *Table) ToggleWide() { } // Actions returns active menu bindings. -func (t *Table) Actions() KeyActions { +func (t *Table) Actions() *KeyActions { return t.actions } @@ -161,7 +235,7 @@ func (t *Table) ExtraHints() map[string]string { } // GetFilteredData fetch filtered tabular data. -func (t *Table) GetFilteredData() *render.TableData { +func (t *Table) GetFilteredData() *model1.TableData { return t.filtered(t.GetModel().Peek()) } @@ -171,111 +245,103 @@ func (t *Table) SetDecorateFn(f DecorateFunc) { } // SetColorerFn specifies the default colorer. -func (t *Table) SetColorerFn(f render.ColorerFunc) { +func (t *Table) SetColorerFn(f model1.ColorerFunc) { t.colorerFn = f } // SetSortCol sets in sort column index and order. func (t *Table) SetSortCol(name string, asc bool) { - t.sortCol.name, t.sortCol.asc = name, asc + t.setSortCol(model1.SortColumn{Name: name, ASC: asc}) } // Update table content. -func (t *Table) Update(data *render.TableData, hasMetrics bool) { - t.header = data.Header +func (t *Table) Update(data *model1.TableData, hasMetrics bool) *model1.TableData { if t.decorateFn != nil { t.decorateFn(data) } t.hasMetrics = hasMetrics - t.doUpdate(t.filtered(data)) - t.UpdateTitle() + + return t.doUpdate(t.filtered(data)) } -func (t *Table) doUpdate(data *render.TableData) { - if client.IsAllNamespaces(data.Namespace) { - t.actions[KeyShiftP] = NewKeyAction("Sort Namespace", t.SortColCmd("NAMESPACE", true), false) +func (t *Table) doUpdate(data *model1.TableData) *model1.TableData { + if client.IsAllNamespaces(data.GetNamespace()) { + t.actions.Add( + KeyShiftP, + NewKeyAction("Sort Namespace", t.SortColCmd("NAMESPACE", true), false), + ) } else { t.actions.Delete(KeyShiftP) } - cols := t.header.Columns(t.wide) - if t.viewSetting != nil && len(t.viewSetting.Columns) > 0 { - cols = t.viewSetting.Columns - } - custData := data.Customize(cols, t.wide) - // The sortColumn settings in the configuration file are only used - // if the sortCol has not been modified manually - if t.viewSetting != nil && t.viewSetting.SortColumn != "" && !t.manualSort { - tokens := strings.Split(t.viewSetting.SortColumn, ":") - if custData.Header.IndexOf(tokens[0], false) >= 0 && !t.manualSort { - t.sortCol.name, t.sortCol.asc = tokens[0], true - if len(tokens) == 2 && tokens[1] == "desc" { - t.sortCol.asc = false - } - } - } + cdata, sortCol := data.Customize(t.getVs(), t.getSortCol(), t.getMSort(), true) + t.setSortCol(sortCol) - if t.sortCol.name == "" && client.IsAllNamespaces(data.Namespace) { - t.sortCol.name = "NAMESPACE" - } - if t.sortCol.name == "" || (t.sortCol.name == "NAMESPACE" && !client.IsAllNamespaces(data.Namespace)) && len(custData.Header) > 0 { - if idx := custData.Header.IndexOf("NAME", false); idx >= 0 { - t.sortCol.name = custData.Header[idx].Name - } else { - t.sortCol.name = custData.Header[0].Name - } - } + return cdata +} +func (t *Table) UpdateUI(cdata, data *model1.TableData) { t.Clear() fg := t.styles.Table().Header.FgColor.Color() bg := t.styles.Table().Header.BgColor.Color() var col int - for _, h := range custData.Header { + for _, h := range cdata.Header() { + if !t.wide && h.Wide { + continue + } if h.Name == "NAMESPACE" && !t.GetModel().ClusterWide() { continue } if h.MX && !t.hasMetrics { continue } + if h.VS && vul.ImgScanner == nil { + continue + } + t.AddHeaderCell(col, h) c := t.GetCell(0, col) c.SetBackgroundColor(bg) c.SetTextColor(fg) col++ } - colIndex := custData.Header.IndexOf(t.sortCol.name, false) - custData.RowEvents.Sort( - custData.Namespace, - colIndex, - custData.Header.IsTimeCol(colIndex), - custData.Header.IsMetricsCol(colIndex), - custData.Header.IsCapacityCol(colIndex), - t.sortCol.asc, - ) - - pads := make(MaxyPad, len(custData.Header)) - ComputeMaxColumns(pads, t.sortCol.name, custData.Header, custData.RowEvents) - for row, re := range custData.RowEvents { - idx, _ := data.RowEvents.FindIndex(re.Row.ID) - t.buildRow(row+1, re, data.RowEvents[idx], custData.Header, pads) - } + cdata.Sort(t.getSortCol()) + + pads := make(MaxyPad, cdata.HeaderCount()) + ComputeMaxColumns(pads, t.getSortCol().Name, cdata) + cdata.RowsRange(func(row int, re model1.RowEvent) bool { + ore, ok := data.FindRow(re.Row.ID) + if !ok { + log.Error().Msgf("unable to find original re: %q", re.Row.ID) + return true + } + t.buildRow(row+1, re, ore, cdata.Header(), pads) + + return true + }) + t.updateSelection(true) + t.UpdateTitle() } -func (t *Table) buildRow(r int, re, ore render.RowEvent, h render.Header, pads MaxyPad) { - color := render.DefaultColorer +func (t *Table) buildRow(r int, re, ore model1.RowEvent, h model1.Header, pads MaxyPad) { + color := model1.DefaultColorer if t.colorerFn != nil { color = t.colorerFn } marked := t.IsMarked(re.Row.ID) var col int + ns := t.GetModel().GetNamespace() for c, field := range re.Row.Fields { if c >= len(h) { log.Error().Msgf("field/header overflow detected for %q -- %d::%d. Check your mappings!", t.GVR(), c, len(h)) continue } + if !t.wide && h[c].Wide { + continue + } if h[c].Name == "NAMESPACE" && !t.GetModel().ClusterWide() { continue @@ -283,6 +349,9 @@ func (t *Table) buildRow(r int, re, ore render.RowEvent, h render.Header, pads M if h[c].MX && !t.hasMetrics { continue } + if h[c].VS && vul.ImgScanner == nil { + continue + } if !re.Deltas.IsBlank() && !h.IsTimeCol(c) { field += Deltas(re.Deltas[c], field) @@ -298,7 +367,7 @@ func (t *Table) buildRow(r int, re, ore render.RowEvent, h render.Header, pads M cell := tview.NewTableCell(field) cell.SetExpansion(1) cell.SetAlign(h[c].Align) - fgColor := color(t.GetModel().GetNamespace(), t.header, ore) + fgColor := color(ns, h, &re) cell.SetTextColor(fgColor) if marked { cell.SetTextColor(t.styles.Table().MarkColor.Color()) @@ -314,13 +383,14 @@ func (t *Table) buildRow(r int, re, ore render.RowEvent, h render.Header, pads M // SortColCmd designates a sorted column. func (t *Table) SortColCmd(name string, asc bool) func(evt *tcell.EventKey) *tcell.EventKey { return func(evt *tcell.EventKey) *tcell.EventKey { - t.manualSort = true - t.sortCol.asc = !t.sortCol.asc - if t.sortCol.name != name { - t.sortCol.asc = asc + sc := t.getSortCol() + sc.ASC = !sc.ASC + if sc.Name != name { + sc.ASC = asc } - t.sortCol.name = name - t.manualSort = true + sc.Name = name + t.setSortCol(sc) + t.setMSort(true) t.Refresh() return nil } @@ -328,7 +398,7 @@ func (t *Table) SortColCmd(name string, asc bool) func(evt *tcell.EventKey) *tce // SortInvertCmd reverses sorting order. func (t *Table) SortInvertCmd(evt *tcell.EventKey) *tcell.EventKey { - t.sortCol.asc = !t.sortCol.asc + t.toggleSortCol() t.Refresh() return nil @@ -343,21 +413,23 @@ func (t *Table) ClearMarks() { // Refresh update the table data. func (t *Table) Refresh() { data := t.model.Peek() - if len(data.Header) == 0 { + if data.HeaderCount() == 0 { return } // BOZO!! Really want to tell model reload now. Refactor! - t.Update(data, t.hasMetrics) + cdata := t.Update(data, t.hasMetrics) + t.UpdateUI(cdata, data) } -// GetSelectedRow returns the entire selected row. -func (t *Table) GetSelectedRow(path string) (render.Row, bool) { +// GetSelectedRow returns the entire selected row or nil if nothing selected. +func (t *Table) GetSelectedRow(path string) *model1.Row { data := t.model.Peek() - i, ok := data.RowEvents.FindIndex(path) + re, ok := data.FindRow(path) if !ok { - return render.Row{}, ok + return nil } - return data.RowEvents[i].Row, true + + return &re.Row } // NameColIndex returns the index of the resource name column. @@ -369,39 +441,25 @@ func (t *Table) NameColIndex() int { if t.GetModel().ClusterWide() { col++ } + return col } // AddHeaderCell configures a table cell header. -func (t *Table) AddHeaderCell(col int, h render.HeaderColumn) { - sortCol := h.Name == t.sortCol.name - c := tview.NewTableCell(sortIndicator(sortCol, t.sortCol.asc, t.styles.Table(), h.Name)) +func (t *Table) AddHeaderCell(col int, h model1.HeaderColumn) { + sc := t.getSortCol() + sortCol := h.Name == sc.Name + c := tview.NewTableCell(sortIndicator(sortCol, sc.ASC, t.styles.Table(), h.Name)) c.SetExpansion(1) c.SetAlign(h.Align) t.SetCell(0, col, c) } -func (t *Table) filtered(data *render.TableData) *render.TableData { - filtered := data - if t.toast { - filtered = filterToast(data) - } - if t.cmdBuff.Empty() || IsLabelSelector(t.cmdBuff.GetText()) { - return filtered - } - - q := t.cmdBuff.GetText() - if IsFuzzySelector(q) { - return fuzzyFilter(q[2:], filtered) - } - - filtered, err := rxFilter(q, IsInverseSelector(q), filtered) - if err != nil { - log.Error().Err(errors.New("Invalid filter expression")).Msg("Regexp") - // t.cmdBuff.ClearText(true) - } - - return filtered +func (t *Table) filtered(data *model1.TableData) *model1.TableData { + return data.Filter(model1.FilterOpts{ + Toast: t.toast, + Filter: t.cmdBuff.GetText(), + }) } // CmdBuff returns the associated command buffer. @@ -424,7 +482,7 @@ func (t *Table) UpdateTitle() { } func (t *Table) styleTitle() string { - rc := t.GetRowCount() + rc := int64(t.GetRowCount()) if rc > 0 { rc-- } @@ -448,18 +506,21 @@ func (t *Table) styleTitle() string { } var title string if ns == client.ClusterScope { - title = SkinTitle(fmt.Sprintf(TitleFmt, base, rc), t.styles.Frame()) + title = SkinTitle(fmt.Sprintf(TitleFmt, base, render.AsThousands(rc)), t.styles.Frame()) } else { - title = SkinTitle(fmt.Sprintf(NSTitleFmt, base, ns, rc), t.styles.Frame()) + title = SkinTitle(fmt.Sprintf(NSTitleFmt, base, ns, render.AsThousands(rc)), t.styles.Frame()) } buff := t.cmdBuff.GetText() + if internal.IsLabelSelector(buff) { + buff = render.Truncate(TrimLabelSelector(buff), maxTruncate) + } else if l := t.GetModel().GetLabelFilter(); l != "" { + buff = render.Truncate(l, maxTruncate) + } + if buff == "" { return title } - if IsLabelSelector(buff) { - buff = TrimLabelSelector(buff) - } return title + SkinTitle(fmt.Sprintf(SearchFmt, buff), t.styles.Frame()) } diff --git a/internal/ui/table_helper.go b/internal/ui/table_helper.go index 21af967c90..479ed2d0bd 100644 --- a/internal/ui/table_helper.go +++ b/internal/ui/table_helper.go @@ -1,16 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( "context" "fmt" - "regexp" "strings" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/render" "github.com/rs/zerolog/log" - "github.com/sahilm/fuzzy" ) const ( @@ -21,10 +21,10 @@ const ( SearchFmt = "<[filter:bg:r]/%s[fg:bg:-]> " // NSTitleFmt represents a namespaced view title. - NSTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] " + NSTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-][[count:bg:b]%s[fg:bg:-]][fg:bg:-] " // TitleFmt represents a standard view title. - TitleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%d[fg:bg:-]][fg:bg:-] " + TitleFmt = "[fg:bg:b] %s[fg:bg:-][[count:bg:b]%s[fg:bg:-]][fg:bg:-] " descIndicator = "↓" ascIndicator = "↑" @@ -36,15 +36,6 @@ const ( NoNSFmat = "%s-%d.csv" ) -var ( - // LabelRx identifies a label query. - LabelRx = regexp.MustCompile(`\A\-l`) - - inverseRx = regexp.MustCompile(`\A\!`) - - fuzzyRx = regexp.MustCompile(`\A\-f`) -) - func mustExtractStyles(ctx context.Context) *config.Styles { styles, ok := ctx.Value(internal.KeyStyles).(*config.Styles) if !ok { @@ -63,33 +54,13 @@ func TrimCell(tv *SelectTable, row, col int) string { return strings.TrimSpace(c.Text) } -// IsLabelSelector checks if query is a label query. -func IsLabelSelector(s string) bool { - if s == "" { - return false - } - return LabelRx.MatchString(s) -} - -// IsFuzzySelector checks if query is fuzzy. -func IsFuzzySelector(s string) bool { - if s == "" { - return false - } - return fuzzyRx.MatchString(s) -} - -// IsInverseSelector checks if inverse char has been provided. -func IsInverseSelector(s string) bool { - if s == "" { - return false - } - return inverseRx.MatchString(s) -} - // TrimLabelSelector extracts label query. func TrimLabelSelector(s string) string { - return strings.TrimSpace(s[2:]) + if strings.Index(s, "-l") == 0 { + return strings.TrimSpace(s[2:]) + } + + return s } // SkinTitle decorates a title. @@ -127,77 +98,3 @@ func formatCell(field string, padding int) string { return field } - -func filterToast(data *render.TableData) *render.TableData { - validX := data.Header.IndexOf("VALID", true) - if validX == -1 { - return data - } - - toast := render.TableData{ - Header: data.Header, - RowEvents: make(render.RowEvents, 0, len(data.RowEvents)), - Namespace: data.Namespace, - } - for _, re := range data.RowEvents { - if re.Row.Fields[validX] != "" { - toast.RowEvents = append(toast.RowEvents, re) - } - } - - return &toast -} - -func rxFilter(q string, inverse bool, data *render.TableData) (*render.TableData, error) { - if inverse { - q = q[1:] - } - rx, err := regexp.Compile(`(?i)(` + q + `)`) - if err != nil { - return data, fmt.Errorf("%w -- %s", err, q) - } - - filtered := render.TableData{ - Header: data.Header, - RowEvents: make(render.RowEvents, 0, len(data.RowEvents)), - Namespace: data.Namespace, - } - ageIndex := -1 - if data.Header.HasAge() { - ageIndex = data.Header.IndexOf("AGE", true) - } - const spacer = " " - for _, re := range data.RowEvents { - ff := re.Row.Fields - if ageIndex > 0 { - ff = append(ff[0:ageIndex], ff[ageIndex+1:]...) - } - fields := strings.Join(ff, spacer) - if (inverse && !rx.MatchString(fields)) || - ((!inverse) && rx.MatchString(fields)) { - filtered.RowEvents = append(filtered.RowEvents, re) - } - } - - return &filtered, nil -} - -func fuzzyFilter(q string, data *render.TableData) *render.TableData { - q = strings.TrimSpace(q) - ss := make([]string, 0, len(data.RowEvents)) - for _, re := range data.RowEvents { - ss = append(ss, re.Row.ID) - } - - filtered := render.TableData{ - Header: data.Header, - RowEvents: make(render.RowEvents, 0, len(data.RowEvents)), - Namespace: data.Namespace, - } - mm := fuzzy.Find(q, ss) - for _, m := range mm { - filtered.RowEvents = append(filtered.RowEvents, data.RowEvents[m.Index]) - } - - return &filtered -} diff --git a/internal/ui/table_helper_test.go b/internal/ui/table_helper_test.go index 8f1c18d9a4..7bec2d4082 100644 --- a/internal/ui/table_helper_test.go +++ b/internal/ui/table_helper_test.go @@ -1,26 +1,34 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( "testing" + "github.com/derailed/k9s/internal/render" "github.com/stretchr/testify/assert" ) -func TestIsLabelSelector(t *testing.T) { +func TestTruncate(t *testing.T) { uu := map[string]struct { - sel string - e bool + s, e string }{ - "cool": {"-l app=fred,env=blee", true}, - "noMode": {"app=fred,env=blee", false}, - "noSpace": {"-lapp=fred,env=blee", true}, - "wrongLabel": {"-f app=fred,env=blee", false}, + "empty": {}, + "max": { + s: "/app.kubernetes.io/instance=prom,app.kubernetes.io/name=prometheus,app.kubernetes.io/component=server", + e: "/app.kubernetes.io/instance=prom,app.kubernetes.i…", + }, + "less": { + s: "app=fred,env=blee", + e: "app=fred,env=blee", + }, } for k := range uu { u := uu[k] t.Run(k, func(t *testing.T) { - assert.Equal(t, u.e, IsLabelSelector(u.sel)) + assert.Equal(t, u.e, render.Truncate(u.s, 50)) }) } } diff --git a/internal/ui/table_test.go b/internal/ui/table_test.go index 6567bb9a5a..9b604d84a9 100644 --- a/internal/ui/table_test.go +++ b/internal/ui/table_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui_test import ( @@ -10,7 +13,7 @@ import ( "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/ui" "github.com/stretchr/testify/assert" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -29,10 +32,11 @@ func TestTableUpdate(t *testing.T) { v.Init(makeContext()) data := makeTableData() - v.Update(data, false) + cdata := v.Update(data, false) + v.UpdateUI(cdata, data) - assert.Equal(t, len(data.RowEvents)+1, v.GetRowCount()) - assert.Equal(t, len(data.Header), v.GetColumnCount()) + assert.Equal(t, data.RowCount()+1, v.GetRowCount()) + assert.Equal(t, data.HeaderCount(), v.GetColumnCount()) } func TestTableSelection(t *testing.T) { @@ -40,13 +44,16 @@ func TestTableSelection(t *testing.T) { v.Init(makeContext()) m := &mockModel{} v.SetModel(m) - v.Update(m.Peek(), false) - v.SelectRow(1, true) - - r, ok := v.GetSelectedRow("r1") - assert.True(t, ok) + data := m.Peek() + cdata := v.Update(data, false) + v.UpdateUI(cdata, data) + v.SelectRow(1, 0, true) + + r := v.GetSelectedRow("r1") + if r != nil { + assert.Equal(t, model1.Row{ID: "r1", Fields: model1.Fields{"blee", "duh", "fred"}}, *r) + } assert.Equal(t, "r1", v.GetSelectedItem()) - assert.Equal(t, render.Row{ID: "r1", Fields: render.Fields{"blee", "duh", "fred"}}, r) assert.Equal(t, "blee", v.GetSelectedCell(0)) assert.Equal(t, 1, v.GetSelectedRowIndex()) assert.Equal(t, []string{"r1"}, v.GetSelectedItems()) @@ -65,10 +72,11 @@ var _ ui.Tabular = &mockModel{} func (t *mockModel) SetInstance(string) {} func (t *mockModel) SetLabelFilter(string) {} +func (t *mockModel) GetLabelFilter() string { return "" } func (t *mockModel) Empty() bool { return false } -func (t *mockModel) Count() int { return 1 } +func (t *mockModel) RowCount() int { return 1 } func (t *mockModel) HasMetrics() bool { return true } -func (t *mockModel) Peek() *render.TableData { return makeTableData() } +func (t *mockModel) Peek() *model1.TableData { return makeTableData() } func (t *mockModel) Refresh(context.Context) error { return nil } func (t *mockModel) ClusterWide() bool { return false } func (t *mockModel) GetNamespace() string { return "blee" } @@ -80,45 +88,41 @@ func (t *mockModel) Watch(context.Context) error { return nil } func (t *mockModel) Get(ctx context.Context, path string) (runtime.Object, error) { return nil, nil } - func (t *mockModel) Delete(context.Context, string, *metav1.DeletionPropagation, dao.Grace) error { return nil } - func (t *mockModel) Describe(context.Context, string) (string, error) { return "", nil } - func (t *mockModel) ToYAML(ctx context.Context, path string) (string, error) { return "", nil } func (t *mockModel) InNamespace(string) bool { return true } func (t *mockModel) SetRefreshRate(time.Duration) {} -func makeTableData() *render.TableData { - t := render.NewTableData() - t.Namespace = "" - t.Header = render.Header{ - render.HeaderColumn{Name: "A"}, - render.HeaderColumn{Name: "B"}, - render.HeaderColumn{Name: "C"}, - } - t.RowEvents = render.RowEvents{ - render.RowEvent{ - Row: render.Row{ - ID: "r1", - Fields: render.Fields{"blee", "duh", "fred"}, - }, +func makeTableData() *model1.TableData { + return model1.NewTableDataWithRows( + client.NewGVR("test"), + model1.Header{ + model1.HeaderColumn{Name: "A"}, + model1.HeaderColumn{Name: "B"}, + model1.HeaderColumn{Name: "C"}, }, - render.RowEvent{ - Row: render.Row{ - ID: "r2", - Fields: render.Fields{"blee", "duh", "zorg"}, + model1.NewRowEventsWithEvts( + model1.RowEvent{ + Row: model1.Row{ + ID: "r1", + Fields: model1.Fields{"blee", "duh", "fred"}, + }, }, - }, - } - - return t + model1.RowEvent{ + Row: model1.Row{ + ID: "r2", + Fields: model1.Fields{"blee", "duh", "zorg"}, + }, + }, + ), + ) } func makeContext() context.Context { diff --git a/internal/ui/tree.go b/internal/ui/tree.go index c62416206c..5af3046b04 100644 --- a/internal/ui/tree.go +++ b/internal/ui/tree.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( @@ -15,7 +18,7 @@ type KeyListenerFunc func() type Tree struct { *tview.TreeView - actions KeyActions + actions *KeyActions selectedItem string cmdBuff *model.FishBuff expandNodes bool @@ -28,7 +31,7 @@ func NewTree() *Tree { return &Tree{ TreeView: tview.NewTreeView(), expandNodes: true, - actions: make(KeyActions), + actions: NewKeyActions(), cmdBuff: model.NewFishBuff('/', model.FilterBuffer), } } @@ -72,7 +75,7 @@ func (t *Tree) SetKeyListenerFn(f KeyListenerFunc) { } // Actions returns active menu bindings. -func (t *Tree) Actions() KeyActions { +func (t *Tree) Actions() *KeyActions { return t.actions } @@ -88,14 +91,14 @@ func (t *Tree) ExtraHints() map[string]string { // BindKeys binds default mnemonics. func (t *Tree) BindKeys() { - t.Actions().Add(KeyActions{ + t.Actions().Merge(NewKeyActionsFromMap(KeyMap{ KeySpace: NewKeyAction("Expand/Collapse", t.noopCmd, true), KeyX: NewKeyAction("Expand/Collapse All", t.toggleCollapseCmd, true), - }) + })) } func (t *Tree) keyboard(evt *tcell.EventKey) *tcell.EventKey { - if a, ok := t.actions[AsKey(evt)]; ok { + if a, ok := t.actions.Get(AsKey(evt)); ok { return a.Action(evt) } diff --git a/internal/ui/types.go b/internal/ui/types.go index 297f22b4f8..534d084e29 100644 --- a/internal/ui/types.go +++ b/internal/ui/types.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package ui import ( @@ -6,22 +9,11 @@ import ( "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) -type ( - // SortFn represent a function that can sort columnar data. - SortFn func(rows render.Rows, sortCol SortColumn) - - // SortColumn represents a sortable column. - SortColumn struct { - name string - asc bool - } -) - // Namespaceable represents a namespaceable model. type Namespaceable interface { // ClusterWide returns true if the model represents resource in all namespaces. @@ -54,14 +46,17 @@ type Tabular interface { // SetLabelFilter sets the label filter. SetLabelFilter(string) + // GetLabelFilter fetch the label filter. + GetLabelFilter() string + // Empty returns true if model has no data. Empty() bool - // Count returns the model data count. - Count() int + // RowCount returns the model data count. + RowCount() int // Peek returns current model data. - Peek() *render.TableData + Peek() *model1.TableData // Watch watches a given resource for changes. Watch(context.Context) error diff --git a/internal/view/actions.go b/internal/view/actions.go index ff6b2a1c71..d86d0098ad 100644 --- a/internal/view/actions.go +++ b/internal/view/actions.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -19,7 +22,7 @@ const AllScopes = "all" type Runner interface { App() *App GetSelectedItem() string - Aliases() []string + Aliases() map[string]struct{} EnvFn() EnvFunc } @@ -41,75 +44,124 @@ func includes(aliases []string, s string) bool { return false } -func inScope(scopes, aliases []string) bool { +func inScope(scopes []string, aliases map[string]struct{}) bool { if hasAll(scopes) { return true } for _, s := range scopes { - if includes(aliases, s) { - return true + if _, ok := aliases[s]; ok { + return ok } } return false } -func hotKeyActions(r Runner, aa ui.KeyActions) { +func hotKeyActions(r Runner, aa *ui.KeyActions) error { hh := config.NewHotKeys() - if err := hh.Load(); err != nil { - return - } + aa.Range(func(k tcell.Key, a ui.KeyAction) { + if a.Opts.HotKey { + aa.Delete(k) + } + }) + var errs error + if err := hh.Load(r.App().Config.ContextHotkeysPath()); err != nil { + errs = errors.Join(errs, err) + } for k, hk := range hh.HotKey { key, err := asKey(hk.ShortCut) if err != nil { - log.Warn().Err(err).Msg("HOT-KEY Unable to map hotkey shortcut to a key") + errs = errors.Join(errs, err) continue } - _, ok := aa[key] - if ok { - log.Warn().Err(fmt.Errorf("HOT-KEY Doh! you are trying to override an existing command `%s", k)).Msg("Invalid shortcut") + if _, ok := aa.Get(key); ok { + if !hk.Override { + errs = errors.Join(errs, fmt.Errorf("duplicate hotkey found for %q in %q", hk.ShortCut, k)) + continue + } + log.Debug().Msgf("Action %q has been overridden by hotkey in %q", hk.ShortCut, k) + } + + command, err := r.EnvFn()().Substitute(hk.Command) + if err != nil { + log.Warn().Err(err).Msg("Invalid shortcut command") continue } - aa[key] = ui.NewSharedKeyAction( + + aa.Add(key, ui.NewKeyActionWithOpts( hk.Description, - gotoCmd(r, hk.Command, ""), - false) + gotoCmd(r, command, "", !hk.KeepHistory), + ui.ActionOpts{ + Shared: true, + HotKey: true, + }, + )) } + + return errs } -func gotoCmd(r Runner, cmd, path string) ui.ActionHandler { +func gotoCmd(r Runner, cmd, path string, clearStack bool) ui.ActionHandler { return func(evt *tcell.EventKey) *tcell.EventKey { - r.App().gotoResource(cmd, path, true) + r.App().gotoResource(cmd, path, clearStack) return nil } } -func pluginActions(r Runner, aa ui.KeyActions) { +func pluginActions(r Runner, aa *ui.KeyActions) error { + aa.Range(func(k tcell.Key, a ui.KeyAction) { + if a.Opts.Plugin { + aa.Delete(k) + } + }) + + path, err := r.App().Config.ContextPluginsPath() + if err != nil { + return err + } pp := config.NewPlugins() - if err := pp.Load(); err != nil { - return + if err := pp.Load(path); err != nil { + return err } - for k, plugin := range pp.Plugin { - if !inScope(plugin.Scopes, r.Aliases()) { + var ( + errs error + aliases = r.Aliases() + ro = r.App().Config.K9s.IsReadOnly() + ) + for k, plugin := range pp.Plugins { + if !inScope(plugin.Scopes, aliases) { continue } key, err := asKey(plugin.ShortCut) if err != nil { - log.Warn().Err(err).Msg("Unable to map plugin shortcut to a key") + errs = errors.Join(errs, err) continue } - _, ok := aa[key] - if ok { - log.Warn().Msgf("Invalid shortcut. You are trying to override an existing command `%s", k) + if _, ok := aa.Get(key); ok { + if !plugin.Override { + errs = errors.Join(errs, fmt.Errorf("duplicate plugin key found for %q in %q", plugin.ShortCut, k)) + continue + } + log.Debug().Msgf("Action %q has been overridden by plugin in %q", plugin.ShortCut, k) + } + + if plugin.Dangerous && ro { continue } - aa[key] = ui.NewKeyAction( + aa.Add(key, ui.NewKeyActionWithOpts( plugin.Description, pluginAction(r, plugin), - true) + ui.ActionOpts{ + Visible: true, + Plugin: true, + Dangerous: plugin.Dangerous, + }, + )) } + + return errs } func pluginAction(r Runner, p config.Plugin) ui.ActionHandler { @@ -134,15 +186,14 @@ func pluginAction(r Runner, p config.Plugin) ui.ActionHandler { cb := func() { opts := shellOpts{ - clear: true, binary: p.Command, background: p.Background, pipes: p.Pipes, args: args, } - suspend, errChan := run(r.App(), opts) + suspend, errChan, statusChan := run(r.App(), opts) if !suspend { - r.App().Flash().Info("Plugin command failed!") + r.App().Flash().Infof("Plugin command failed: %q", p.Description) return } var errs error @@ -153,7 +204,18 @@ func pluginAction(r Runner, p config.Plugin) ui.ActionHandler { r.App().cowCmd(errs.Error()) return } - r.App().Flash().Info("Plugin command launched successfully!") + go func() { + for st := range statusChan { + if !p.OverwriteOutput { + r.App().Flash().Infof("Plugin command launched successfully: %q", st) + } else if strings.Contains(st, outputPrefix) { + infoMsg := strings.TrimPrefix(st, outputPrefix) + r.App().Flash().Info(strings.TrimSpace(infoMsg)) + return + } + } + }() + } if p.Confirm { msg := fmt.Sprintf("Run?\n%s %s", p.Command, strings.Join(args, " ")) diff --git a/internal/view/actions_test.go b/internal/view/actions_test.go index 7371091b7a..76f8abaf9c 100644 --- a/internal/view/actions_test.go +++ b/internal/view/actions_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -50,15 +53,16 @@ func TestIncludes(t *testing.T) { func TestInScope(t *testing.T) { uu := map[string]struct { - ss, aa []string - e bool + ss []string + aa map[string]struct{} + e bool }{ "empty": {}, - "yes": {e: true, ss: []string{"blee", "duh", "fred"}, aa: []string{"blee", "fred", "duh"}}, - "no": {ss: []string{"blee", "duh", "fred"}, aa: []string{"blee1", "fred1"}}, - "empty scopes": {aa: []string{"blee1", "fred1"}}, + "yes": {e: true, ss: []string{"blee", "duh", "fred"}, aa: map[string]struct{}{"blee": {}, "fred": {}, "duh": {}}}, + "no": {ss: []string{"blee", "duh", "fred"}, aa: map[string]struct{}{"blee1": {}, "fred1": {}}}, + "empty scopes": {aa: map[string]struct{}{"blee1": {}, "fred1": {}}}, "empty aliases": {ss: []string{"blee1", "fred1"}}, - "all": {e: true, ss: []string{AllScopes}, aa: []string{"blee1", "fred1"}}, + "all": {e: true, ss: []string{AllScopes}, aa: map[string]struct{}{"blee1": {}, "fred1": {}}}, } for k := range uu { diff --git a/internal/view/alias.go b/internal/view/alias.go index c31623e876..496f2e5c73 100644 --- a/internal/view/alias.go +++ b/internal/view/alias.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -44,14 +47,14 @@ func (a *Alias) aliasContext(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeyAliases, a.App().command.alias) } -func (a *Alias) bindKeys(aa ui.KeyActions) { +func (a *Alias) bindKeys(aa *ui.KeyActions) { aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) aa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL) - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, true), ui.KeyShiftR: ui.NewKeyAction("Sort Resource", a.GetTable().SortColCmd("RESOURCE", true), false), ui.KeyShiftC: ui.NewKeyAction("Sort Command", a.GetTable().SortColCmd("COMMAND", true), false), - ui.KeyShiftA: ui.NewKeyAction("Sort ApiGroup", a.GetTable().SortColCmd("APIGROUP", true), false), + ui.KeyShiftA: ui.NewKeyAction("Sort ApiGroup", a.GetTable().SortColCmd("API-GROUP", true), false), }) } diff --git a/internal/view/alias_test.go b/internal/view/alias_test.go index 00f9168413..6deba28983 100644 --- a/internal/view/alias_test.go +++ b/internal/view/alias_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( @@ -7,15 +10,14 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/view" "github.com/derailed/tcell/v2" "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -71,33 +73,11 @@ func (b *buffL) BufferActive(state bool, kind model.BufferKind) { } func makeContext() context.Context { - a := view.NewApp(config.NewConfig(ks{})) + a := view.NewApp(mock.NewMockConfig()) ctx := context.WithValue(context.Background(), internal.KeyApp, a) return context.WithValue(ctx, internal.KeyStyles, a.Styles) } -type ks struct{} - -func (k ks) CurrentContextName() (string, error) { - return "test", nil -} - -func (k ks) CurrentClusterName() (string, error) { - return "test", nil -} - -func (k ks) CurrentNamespaceName() (string, error) { - return "test", nil -} - -func (k ks) ClusterNames() (map[string]struct{}, error) { - return map[string]struct{}{"test": {}}, nil -} - -func (k ks) NamespaceNames(nn []v1.Namespace) []string { - return []string{"test"} -} - type mockModel struct{} var ( @@ -111,10 +91,11 @@ func (t *mockModel) PrevSuggestion() (string, bool) { return "", false } func (t *mockModel) ClearSuggestions() {} func (t *mockModel) SetInstance(string) {} func (t *mockModel) SetLabelFilter(string) {} +func (t *mockModel) GetLabelFilter() string { return "" } func (t *mockModel) Empty() bool { return false } -func (t *mockModel) Count() int { return 1 } +func (t *mockModel) RowCount() int { return 1 } func (t *mockModel) HasMetrics() bool { return true } -func (t *mockModel) Peek() *render.TableData { return makeTableData() } +func (t *mockModel) Peek() *model1.TableData { return makeTableData() } func (t *mockModel) ClusterWide() bool { return false } func (t *mockModel) GetNamespace() string { return "blee" } func (t *mockModel) SetNamespace(string) {} @@ -142,27 +123,27 @@ func (t *mockModel) ToYAML(ctx context.Context, path string) (string, error) { func (t *mockModel) InNamespace(string) bool { return true } func (t *mockModel) SetRefreshRate(time.Duration) {} -func makeTableData() *render.TableData { - return &render.TableData{ - Namespace: client.ClusterScope, - Header: render.Header{ - render.HeaderColumn{Name: "RESOURCE"}, - render.HeaderColumn{Name: "COMMAND"}, - render.HeaderColumn{Name: "APIGROUP"}, +func makeTableData() *model1.TableData { + return model1.NewTableDataWithRows( + client.NewGVR("test"), + model1.Header{ + model1.HeaderColumn{Name: "RESOURCE"}, + model1.HeaderColumn{Name: "COMMAND"}, + model1.HeaderColumn{Name: "APIGROUP"}, }, - RowEvents: render.RowEvents{ - render.RowEvent{ - Row: render.Row{ + model1.NewRowEventsWithEvts( + model1.RowEvent{ + Row: model1.Row{ ID: "r1", - Fields: render.Fields{"blee", "duh", "fred"}, + Fields: model1.Fields{"blee", "duh", "fred"}, }, }, - render.RowEvent{ - Row: render.Row{ + model1.RowEvent{ + Row: model1.Row{ ID: "r2", - Fields: render.Fields{"fred", "duh", "zorg"}, + Fields: model1.Fields{"fred", "duh", "zorg"}, }, }, - }, - } + ), + ) } diff --git a/internal/view/app.go b/internal/view/app.go index a921d443d0..4ac7e7c2b1 100644 --- a/internal/view/app.go +++ b/internal/view/app.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -20,11 +23,12 @@ import ( "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" + "github.com/derailed/k9s/internal/view/cmd" + "github.com/derailed/k9s/internal/vul" "github.com/derailed/k9s/internal/watch" "github.com/derailed/tcell/v2" "github.com/derailed/tview" "github.com/rs/zerolog/log" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // ExitStatus indicates UI exit conditions. @@ -57,11 +61,12 @@ type App struct { // NewApp returns a K9s app instance. func NewApp(cfg *config.Config) *App { a := App{ - App: ui.NewApp(cfg, cfg.K9s.CurrentContext), + App: ui.NewApp(cfg, cfg.K9s.ActiveContextName()), cmdHistory: model.NewHistory(model.MaxHistory), filterHistory: model.NewHistory(model.MaxHistory), Content: NewPageStack(), } + a.ReloadStyles() a.Views()["statusIndicator"] = ui.NewStatusIndicator(a.App, a.Styles) a.Views()["clusterInfo"] = NewClusterInfo(&a) @@ -69,6 +74,18 @@ func NewApp(cfg *config.Config) *App { return &a } +// ReloadStyles reloads skin file. +func (a *App) ReloadStyles() { + a.RefreshStyles(a) +} + +// UpdateClusterInfo updates clusterInfo panel +func (a *App) UpdateClusterInfo() { + if a.factory != nil { + a.clusterModel.Reset(a.factory) + } +} + // ConOK checks the connection is cool, returns false otherwise. func (a *App) ConOK() bool { return atomic.LoadInt32(&a.conRetry) == 0 @@ -94,13 +111,9 @@ func (a *App) Init(version string, rate int) error { ns := a.Config.ActiveNamespace() a.factory = watch.NewFactory(a.Conn()) - ok, err := a.isValidNS(ns) - if !ok && err == nil { - return fmt.Errorf("invalid namespace %s", ns) - } a.initFactory(ns) - a.clusterModel = model.NewClusterInfo(a.factory, a.version, a.Config.K9s.SkipLatestRevCheck) + a.clusterModel = model.NewClusterInfo(a.factory, a.version, a.Config.K9s) a.clusterModel.AddListener(a.clusterInfo()) a.clusterModel.AddListener(a.statusIndicator()) if a.Conn().ConnectionOK() { @@ -109,7 +122,7 @@ func (a *App) Init(version string, rate int) error { } a.command = NewCommand(a) - if err := a.command.Init(); err != nil { + if err := a.command.Init(a.Config.ContextAliasesPath()); err != nil { return err } a.CmdBuff().SetSuggestionFn(a.suggestCommand()) @@ -117,9 +130,29 @@ func (a *App) Init(version string, rate int) error { a.layout(ctx) a.initSignals() + if a.Config.K9s.ImageScans.Enable { + a.initImgScanner(version) + } + a.ReloadStyles() + return nil } +func (a *App) stopImgScanner() { + if vul.ImgScanner != nil { + vul.ImgScanner.Stop() + } +} + +func (a *App) initImgScanner(version string) { + defer func(t time.Time) { + log.Debug().Msgf("Scanner init time %s", time.Since(t)) + }(time.Now()) + + vul.ImgScanner = vul.NewImageScanner(a.Config.K9s.ImageScans) + go vul.ImgScanner.Init("k9s", version) +} + func (a *App) layout(ctx context.Context) { flash := ui.NewFlash(a.App) go flash.Watch(ctx, a.Flash().Channel()) @@ -148,6 +181,11 @@ func (a *App) initSignals() { } func (a *App) suggestCommand() model.SuggestionFunc { + contextNames, err := a.contextNames() + if err != nil { + log.Error().Err(err).Msg("failed to list contexts") + } + return func(s string) (entries sort.StringSlice) { if s == "" { if a.cmdHistory.Empty() { @@ -156,15 +194,18 @@ func (a *App) suggestCommand() model.SuggestionFunc { return a.cmdHistory.List() } - s = strings.ToLower(s) + ls := strings.ToLower(s) for _, k := range a.command.alias.Aliases.Keys() { - if k == s { - continue - } - if strings.HasPrefix(k, s) { - entries = append(entries, strings.Replace(k, s, "", 1)) + if suggest, ok := cmd.ShouldAddSuggest(ls, k); ok { + entries = append(entries, suggest) } } + + namespaceNames, err := a.factory.Client().ValidNamespaceNames() + if err != nil { + log.Error().Err(err).Msg("failed to list namespaces") + } + entries = append(entries, cmd.SuggestSubCommand(s, namespaceNames, contextNames)...) if len(entries) == 0 { return nil } @@ -173,6 +214,22 @@ func (a *App) suggestCommand() model.SuggestionFunc { } } +func (a *App) contextNames() ([]string, error) { + if !a.Conn().ConnectionOK() { + return nil, errors.New("no connection") + } + contexts, err := a.factory.Client().Config().Contexts() + if err != nil { + return nil, err + } + contextNames := make([]string, 0, len(contexts)) + for ctxName := range contexts { + contextNames = append(contextNames, ctxName) + } + + return contextNames, nil +} + func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey { if k, ok := a.HasAction(ui.AsKey(evt)); ok && !a.Content.IsTopDialog() { return k.Action(evt) @@ -182,14 +239,15 @@ func (a *App) keyboard(evt *tcell.EventKey) *tcell.EventKey { } func (a *App) bindKeys() { - a.AddActions(ui.KeyActions{ + a.AddActions(ui.NewKeyActionsFromMap(ui.KeyMap{ ui.KeyShift9: ui.NewSharedKeyAction("DumpGOR", a.dumpGOR, false), tcell.KeyCtrlE: ui.NewSharedKeyAction("ToggleHeader", a.toggleHeaderCmd, false), tcell.KeyCtrlG: ui.NewSharedKeyAction("toggleCrumbs", a.toggleCrumbsCmd, false), ui.KeyHelp: ui.NewSharedKeyAction("Help", a.helpCmd, false), tcell.KeyCtrlA: ui.NewSharedKeyAction("Aliases", a.aliasCmd, false), tcell.KeyEnter: ui.NewKeyAction("Goto", a.gotoCmd, false), - }) + tcell.KeyCtrlC: ui.NewKeyAction("Quit", a.quitCmd, false), + })) } func (a *App) dumpGOR(evt *tcell.EventKey) *tcell.EventKey { @@ -244,11 +302,13 @@ func (a *App) buildHeader() tview.Primitive { } clWidth := clusterInfoWidth - n, err := a.Conn().Config().CurrentClusterName() - if err == nil { - size := len(n) + clusterInfoPad - if size > clWidth { - clWidth = size + if a.Conn().ConnectionOK() { + n, err := a.Conn().Config().CurrentClusterName() + if err == nil { + size := len(n) + clusterInfoPad + if size > clWidth { + clWidth = size + } } } header.AddItem(a.clusterInfo(), clWidth, 1, false) @@ -275,11 +335,17 @@ func (a *App) Resume() { ctx, a.cancelFn = context.WithCancel(context.Background()) go a.clusterUpdater(ctx) - if err := a.StylesWatcher(ctx, a); err != nil { - log.Warn().Err(err).Msgf("Styles watcher failed") - } - if err := a.CustomViewsWatcher(ctx, a); err != nil { - log.Warn().Err(err).Msgf("CustomView watcher failed") + + if a.Config.K9s.UI.Reactive { + if err := a.ConfigWatcher(ctx, a); err != nil { + log.Warn().Err(err).Msgf("ConfigWatcher failed") + } + if err := a.SkinsDirWatcher(ctx, a); err != nil { + log.Warn().Err(err).Msgf("SkinsWatcher failed") + } + if err := a.CustomViewsWatcher(ctx, a); err != nil { + log.Warn().Err(err).Msgf("CustomView watcher failed") + } } } @@ -342,11 +408,13 @@ func (a *App) refreshCluster(context.Context) error { // Reload alias go func() { - if err := a.command.Reset(false); err != nil { - log.Error().Err(err).Msgf("Command reset failed") + if err := a.command.Reset(a.Config.ContextAliasesPath(), false); err != nil { + log.Warn().Err(err).Msgf("Command reset failed") + a.QueueUpdateDraw(func() { + a.Logo().Warn("Aliases load failed!") + }) } }() - // Update cluster info a.clusterModel.Refresh() @@ -354,83 +422,66 @@ func (a *App) refreshCluster(context.Context) error { } func (a *App) switchNS(ns string) error { - if ns == client.ClusterScope { - ns = client.AllNamespaces - } - if ns == a.Config.ActiveNamespace() { + if a.Config.ActiveNamespace() == ns { return nil } - - ok, err := a.isValidNS(ns) - if err != nil { - return err - } - if !ok { - return fmt.Errorf("invalid namespace %q", ns) + if ns == client.ClusterScope { + ns = client.BlankNamespace } if err := a.Config.SetActiveNamespace(ns); err != nil { return err } - if err := a.Config.Save(); err != nil { - return err - } return a.factory.SetActiveNS(ns) } -func (a *App) isValidNS(ns string) (bool, error) { - if ns == client.AllNamespaces || ns == client.NamespaceAll { - return true, nil - } - - ctx, cancel := context.WithTimeout(context.Background(), a.Conn().Config().CallTimeout()) - defer cancel() - dial, err := a.Conn().Dial() - if err != nil { - return false, err - } - _, err = dial.CoreV1().Namespaces().Get(ctx, ns, metav1.GetOptions{}) - if err != nil { - log.Warn().Err(err).Msgf("Validation failed for namespace: %q", ns) +func (a *App) switchContext(ci *cmd.Interpreter, force bool) error { + name, ok := ci.HasContext() + if !ok || a.Config.ActiveContextName() == name { + if !force { + return nil + } } - return true, nil -} - -func (a *App) switchContext(name string) error { - log.Debug().Msgf("--> Switching Context %q--%q", name, a.Config.ActiveView()) a.Halt() defer a.Resume() { - ns, err := a.Conn().Config().CurrentNamespaceName() + a.Config.Reset() + ct, err := a.Config.K9s.ActivateContext(name) if err != nil { - log.Warn().Msg("No namespace specified in context. Using K9s config") - ns = a.Config.ActiveNamespace() + return err } - a.initFactory(ns) - - if e := a.command.Reset(true); e != nil { - return e + if cns, ok := ci.NSArg(); ok { + ct.Namespace.Active = cns } - if a.Config.ActiveView() == "" || isContextCmd(a.Config.ActiveView()) { + + p := cmd.NewInterpreter(a.Config.ActiveView()) + p.ResetContextArg() + if p.IsContextCmd() { a.Config.SetActiveView("pod") } - a.Config.Reset() - a.Config.K9s.CurrentContext = name - cluster, err := a.Conn().Config().CurrentClusterName() - if err != nil { - return err - } - a.Config.K9s.CurrentCluster = cluster - if err := a.Config.SetActiveNamespace(ns); err != nil { - log.Error().Err(err).Msg("unable to set active ns") + ns := a.Config.ActiveNamespace() + if !a.Conn().IsValidNamespace(ns) { + log.Warn().Msgf("Unable to validate namespace: %q. Using %q as active namespace", ns, ns) + if err := a.Config.SetActiveNamespace(ns); err != nil { + return err + } } - if err := a.Config.Save(); err != nil { + a.Flash().Infof("Using %q namespace", ns) + + if err := a.Config.Save(true); err != nil { log.Error().Err(err).Msg("config save failed!") + } else { + log.Debug().Msgf("Saved context config for: %q", name) + } + a.initFactory(ns) + if err := a.command.Reset(a.Config.ContextAliasesPath(), true); err != nil { + return err } - a.Flash().Infof("Switching context to %s", name) - a.ReloadStyles(name) + log.Debug().Msgf("--> Switching Context %q -- %q -- %q", name, ns, a.Config.ActiveView()) + a.Flash().Infof("Switching context to %q::%q", name, ns) + a.ReloadStyles() a.gotoResource(a.Config.ActiveView(), "", true) a.clusterModel.Reset(a.factory) } @@ -451,9 +502,15 @@ func (a *App) BailOut() { } }() + if err := a.Config.Save(true); err != nil { + log.Error().Err(err).Msg("config save failed!") + } + if err := nukeK9sShell(a); err != nil { log.Error().Err(err).Msgf("nuking k9s shell pod") } + + a.stopImgScanner() a.factory.Terminate() a.App.BailOut() } @@ -602,13 +659,25 @@ func (a *App) dirCmd(path string) error { return a.inject(NewDir(path), true) } -func (a *App) helpCmd(evt *tcell.EventKey) *tcell.EventKey { - top := a.Content.Top() +func (a *App) quitCmd(evt *tcell.EventKey) *tcell.EventKey { + if a.InCmdMode() { + return evt + } - if a.CmdBuff().InCmdMode() || (top != nil && top.InCmdMode()) { + if !a.Config.K9s.NoExitOnCtrlC { + a.BailOut() + } + + // overwrite the default ctrl-c behavior of tview + return nil +} + +func (a *App) helpCmd(evt *tcell.EventKey) *tcell.EventKey { + if evt != nil && evt.Rune() == '?' && a.Prompt().InCmdMode() { return evt } + top := a.Content.Top() if top != nil && top.Name() == "help" { a.Content.Pop() return nil @@ -618,14 +687,11 @@ func (a *App) helpCmd(evt *tcell.EventKey) *tcell.EventKey { a.Flash().Err(err) } + a.Prompt().Deactivate() return nil } func (a *App) aliasCmd(evt *tcell.EventKey) *tcell.EventKey { - if a.CmdBuff().InCmdMode() { - return evt - } - if a.Content.Top() != nil && a.Content.Top().Name() == aliasTitle { a.Content.Pop() return nil @@ -638,8 +704,8 @@ func (a *App) aliasCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (a *App) gotoResource(cmd, path string, clearStack bool) { - err := a.command.run(cmd, path, clearStack) +func (a *App) gotoResource(c, path string, clearStack bool) { + err := a.command.run(cmd.NewInterpreter(c), path, clearStack) if err != nil { dialog.ShowError(a.Styles.Dialog(), a.Content.Pages, err.Error()) } @@ -648,14 +714,12 @@ func (a *App) gotoResource(cmd, path string, clearStack bool) { func (a *App) inject(c model.Component, clearStack bool) error { ctx := context.WithValue(context.Background(), internal.KeyApp, a) if err := c.Init(ctx); err != nil { - log.Error().Err(err).Msgf("component init failed for %q", c.Name()) - //dialog.ShowError(a.Styles.Dialog(), a.Content.Pages, err.Error()) + log.Error().Err(err).Msgf("Component init failed for %q", c.Name()) return err } if clearStack { a.Content.Stack.Clear() } - a.Content.Push(c) return nil diff --git a/internal/view/app_test.go b/internal/view/app_test.go index 4726eb9ac0..e1e932f0e6 100644 --- a/internal/view/app_test.go +++ b/internal/view/app_test.go @@ -1,16 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( "testing" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" ) func TestAppNew(t *testing.T) { - a := view.NewApp(config.NewConfig(ks{})) + a := view.NewApp(mock.NewMockConfig()) _ = a.Init("blee", 10) - assert.Equal(t, 11, len(a.GetActions())) + assert.Equal(t, 12, a.GetActions().Len()) } diff --git a/internal/view/benchmark.go b/internal/view/benchmark.go index 5601c42a18..535b39f864 100644 --- a/internal/view/benchmark.go +++ b/internal/view/benchmark.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -9,9 +12,10 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/perf" + "github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" + "github.com/rs/zerolog/log" ) // Benchmark represents a service benchmark results view. @@ -37,14 +41,14 @@ func (b *Benchmark) benchContext(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeyDir, benchDir(b.App().Config)) } -func (b *Benchmark) viewBench(app *App, model ui.Tabular, gvr, path string) { +func (b *Benchmark) viewBench(app *App, model ui.Tabular, gvr client.GVR, path string) { data, err := readBenchFile(app.Config, b.benchFile()) if err != nil { app.Flash().Errf("Unable to load bench file %s", err) return } - details := NewDetails(b.App(), "Results", fileToSubject(path), false).Update(data) + details := NewDetails(b.App(), "Results", fileToSubject(path), contentYAML, false).Update(data) if err := app.inject(details, false); err != nil { app.Flash().Err(err) } @@ -65,7 +69,15 @@ func fileToSubject(path string) string { } func benchDir(cfg *config.Config) string { - return filepath.Join(perf.K9sBenchDir, cfg.K9s.CurrentCluster) + ct, err := cfg.K9s.ActiveContext() + if err != nil { + log.Error().Err(err).Msgf("no active context located") + } + return filepath.Join( + config.AppBenchmarksDir, + data.SanitizeFileName(ct.ClusterName), + data.SanitizeFileName(cfg.K9s.ActiveContextName()), + ) } func readBenchFile(cfg *config.Config, n string) (string, error) { diff --git a/internal/view/browser.go b/internal/view/browser.go index 0678bfacd3..7f0876d1ab 100644 --- a/internal/view/browser.go +++ b/internal/view/browser.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -11,10 +14,10 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" "github.com/derailed/tcell/v2" @@ -32,6 +35,7 @@ type Browser struct { contextFn ContextFunc cancelFn context.CancelFunc mx sync.RWMutex + updating bool } // NewBrowser returns a new browser. @@ -41,6 +45,18 @@ func NewBrowser(gvr client.GVR) ResourceViewer { } } +func (b *Browser) setUpdating(f bool) { + b.mx.Lock() + defer b.mx.Unlock() + b.updating = f +} + +func (b *Browser) getUpdating() bool { + b.mx.RLock() + defer b.mx.RUnlock() + return b.updating +} + // Init watches all running pods in given namespace. func (b *Browser) Init(ctx context.Context) error { var err error @@ -48,8 +64,8 @@ func (b *Browser) Init(ctx context.Context) error { if err != nil { return err } - colorerFn := render.DefaultColorer - if r, ok := model.Registry[b.GVR().String()]; ok { + colorerFn := model1.DefaultColorer + if r, ok := model.Registry[b.GVR().String()]; ok && r.Renderer != nil { colorerFn = r.Renderer.ColorerFunc() } b.GetTable().SetColorerFn(colorerFn) @@ -59,7 +75,7 @@ func (b *Browser) Init(ctx context.Context) error { } ns := client.CleanseNamespace(b.app.Config.ActiveNamespace()) if dao.IsK8sMeta(b.meta) && b.app.ConOK() { - if _, e := b.app.factory.CanForResource(ns, b.GVR().String(), client.MonitorAccess); e != nil { + if _, e := b.app.factory.CanForResource(ns, b.GVR().String(), client.ListAccess); e != nil { return e } } @@ -115,8 +131,8 @@ func (b *Browser) suggestFilter() model.SuggestionFunc { } } -func (b *Browser) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (b *Browser) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", b.resetCmd, false), tcell.KeyEnter: ui.NewSharedKeyAction("Filter", b.filterCmd, false), tcell.KeyHelp: ui.NewSharedKeyAction("Help", b.helpCmd, false), @@ -130,7 +146,6 @@ func (b *Browser) SetInstance(path string) { // Start initializes browser updates. func (b *Browser) Start() { - b.app.Config.ValidateFavorites() ns := b.app.Config.ActiveNamespace() if n := b.GetModel().GetNamespace(); !client.IsClusterScoped(n) { ns = n @@ -163,12 +178,21 @@ func (b *Browser) Stop() { b.Table.Stop() } +func (b *Browser) SetFilter(s string) { + b.CmdBuff().SetText(s, "") +} + +func (b *Browser) SetLabelFilter(labels map[string]string) { + b.CmdBuff().SetText(toLabelsStr(labels), "") + b.GetModel().SetLabelFilter(toLabelsStr(labels)) +} + // BufferChanged indicates the buffer was changed. func (b *Browser) BufferChanged(_, _ string) {} // BufferCompleted indicates input was accepted. func (b *Browser) BufferCompleted(text, _ string) { - if ui.IsLabelSelector(text) { + if internal.IsLabelSelector(text) { b.GetModel().SetLabelFilter(ui.TrimLabelSelector(text)) } else { b.GetModel().SetLabelFilter("") @@ -180,26 +204,48 @@ func (b *Browser) BufferActive(state bool, k model.BufferKind) { if state { return } - if err := b.GetModel().Refresh(b.prepareContext()); err != nil { + if err := b.GetModel().Refresh(b.GetContext()); err != nil { log.Error().Err(err).Msgf("Refresh failed for %s", b.GVR()) } + data := b.GetModel().Peek() + cdata := b.Update(data, b.App().Conn().HasMetrics()) b.app.QueueUpdateDraw(func() { - b.Update(b.GetModel().Peek(), b.App().Conn().HasMetrics()) + if b.getUpdating() { + return + } + b.setUpdating(true) + defer b.setUpdating(false) + b.UpdateUI(cdata, data) if b.GetRowCount() > 1 { b.App().filterHistory.Push(b.CmdBuff().GetText()) } + }) } func (b *Browser) prepareContext() context.Context { ctx := b.defaultContext() - ctx, b.cancelFn = context.WithCancel(ctx) + + b.mx.Lock() + { + if b.cancelFn != nil { + b.cancelFn() + } + ctx, b.cancelFn = context.WithCancel(ctx) + } + b.mx.Unlock() + if b.contextFn != nil { ctx = b.contextFn(ctx) } if path, ok := ctx.Value(internal.KeyPath).(string); ok && path != "" { b.Path = path } + b.mx.Lock() + { + b.SetContext(ctx) + } + b.mx.Unlock() return ctx } @@ -218,15 +264,15 @@ func (b *Browser) SetContextFn(f ContextFunc) { b.contextFn = f } func (b *Browser) GetTable() *Table { return b.Table } // Aliases returns all available aliases. -func (b *Browser) Aliases() []string { - return append(b.meta.ShortNames, b.meta.SingularName, b.meta.Name) +func (b *Browser) Aliases() map[string]struct{} { + return aliasesFor(b.meta, b.app.command.AliasesFor(b.meta.Name)) } // ---------------------------------------------------------------------------- // Model Protocol... // TableDataChanged notifies view new data is available. -func (b *Browser) TableDataChanged(data *render.TableData) { +func (b *Browser) TableDataChanged(data *model1.TableData) { var cancel context.CancelFunc b.mx.RLock() cancel = b.cancelFn @@ -236,9 +282,15 @@ func (b *Browser) TableDataChanged(data *render.TableData) { return } + cdata := b.Update(data, b.app.Conn().HasMetrics()) b.app.QueueUpdateDraw(func() { + if b.getUpdating() { + return + } + b.setUpdating(true) + defer b.setUpdating(false) b.refreshActions() - b.Update(data, b.app.Conn().HasMetrics()) + b.UpdateUI(cdata, data) }) } @@ -259,7 +311,7 @@ func (b *Browser) viewCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } - v := NewLiveView(b.app, "YAML", model.NewYAML(b.GVR(), path)) + v := NewLiveView(b.app, yamlAction, model.NewYAML(b.GVR(), path)) if err := v.app.inject(v, false); err != nil { v.app.Flash().Err(err) } @@ -276,12 +328,17 @@ func (b *Browser) helpCmd(evt *tcell.EventKey) *tcell.EventKey { func (b *Browser) resetCmd(evt *tcell.EventKey) *tcell.EventKey { if !b.CmdBuff().InCmdMode() { + hasFilter := !b.CmdBuff().Empty() b.CmdBuff().ClearText(false) + if hasFilter { + b.GetModel().SetLabelFilter("") + b.Refresh() + } return b.App().PrevCmd(evt) } b.CmdBuff().Reset() - if ui.IsLabelSelector(b.CmdBuff().GetText()) { + if internal.IsLabelSelector(b.CmdBuff().GetText()) { b.Start() } b.Refresh() @@ -295,7 +352,7 @@ func (b *Browser) filterCmd(evt *tcell.EventKey) *tcell.EventKey { } b.CmdBuff().SetActive(false) - if ui.IsLabelSelector(b.CmdBuff().GetText()) { + if internal.IsLabelSelector(b.CmdBuff().GetText()) { b.Start() return nil } @@ -314,7 +371,7 @@ func (b *Browser) enterCmd(evt *tcell.EventKey) *tcell.EventKey { if b.enterFn != nil { f = b.enterFn } - f(b.app, b.GetModel(), b.GVR().String(), path) + f(b.app, b.GetModel(), b.GVR(), path) return nil } @@ -354,7 +411,7 @@ func (b *Browser) describeCmd(evt *tcell.EventKey) *tcell.EventKey { if path == "" { return evt } - describeResource(b.app, b.GetModel(), b.GVR().String(), path) + describeResource(b.app, b.GetModel(), b.GVR(), path) return nil } @@ -364,33 +421,42 @@ func (b *Browser) editCmd(evt *tcell.EventKey) *tcell.EventKey { if path == "" { return evt } + + b.Stop() + defer b.Start() + if err := editRes(b.app, b.GVR(), path); err != nil { + b.App().Flash().Err(err) + } + + return nil +} + +func editRes(app *App, gvr client.GVR, path string) error { + if path == "" { + return fmt.Errorf("nothing selected %q", path) + } ns, n := client.Namespaced(path) if client.IsClusterScoped(ns) { - ns = client.AllNamespaces + ns = client.BlankNamespace } - if b.GVR().String() == "v1/namespaces" { + if gvr.String() == "v1/namespaces" { ns = n } - if ok, err := b.app.Conn().CanI(ns, b.GVR().String(), []string{"patch"}); !ok || err != nil { - b.App().Flash().Errf("Current user can't edit resource %s", b.GVR()) - return nil + if ok, err := app.Conn().CanI(ns, gvr.String(), n, client.PatchAccess); !ok || err != nil { + return fmt.Errorf("current user can't edit resource %s", gvr) } - b.Stop() - defer b.Start() - { - args := make([]string, 0, 10) - args = append(args, "edit") - args = append(args, b.GVR().FQN(n)) - if ns != client.AllNamespaces { - args = append(args, "-n", ns) - } - if err := runK(b.app, shellOpts{clear: true, args: args}); err != nil { - b.app.Flash().Errf("Edit command failed: %s", err) - } + args := make([]string, 0, 10) + args = append(args, "edit") + args = append(args, gvr.FQN(n)) + if ns != client.BlankNamespace { + args = append(args, "-n", ns) + } + if err := runK(app, shellOpts{clear: true, args: args}); err != nil { + app.Flash().Errf("Edit command failed: %s", err) } - return evt + return nil } func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -401,7 +467,7 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey { } ns := b.namespaces[i] - auth, err := b.App().factory.Client().CanI(ns, b.GVR().String(), client.MonitorAccess) + auth, err := b.App().factory.Client().CanI(ns, b.GVR().String(), "", client.ListAccess) if !auth { if err == nil { err = fmt.Errorf("current user can't access namespace %s", ns) @@ -422,14 +488,11 @@ func (b *Browser) switchNamespaceCmd(evt *tcell.EventKey) *tcell.EventKey { b.app.Flash().Infof("Viewing namespace `%s`...", ns) b.refresh() b.UpdateTitle() - b.SelectRow(1, true) + b.SelectRow(1, 0, true) b.app.CmdBuff().Reset() if err := b.app.Config.SetActiveNamespace(b.GetModel().GetNamespace()); err != nil { log.Error().Err(err).Msg("Config save NS failed!") } - if err := b.app.Config.Save(); err != nil { - log.Error().Err(err).Msg("Config save failed!") - } return nil } @@ -450,14 +513,13 @@ func (b *Browser) setNamespace(ns string) { func (b *Browser) defaultContext() context.Context { ctx := context.WithValue(context.Background(), internal.KeyFactory, b.app.factory) - ctx = context.WithValue(ctx, internal.KeyGVR, b.GVR().String()) - if b.Path != "" { - ctx = context.WithValue(ctx, internal.KeyPath, b.Path) - } - if ui.IsLabelSelector(b.CmdBuff().GetText()) { + ctx = context.WithValue(ctx, internal.KeyGVR, b.GVR()) + ctx = context.WithValue(ctx, internal.KeyPath, b.Path) + if internal.IsLabelSelector(b.CmdBuff().GetText()) { ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(b.CmdBuff().GetText())) } ctx = context.WithValue(ctx, internal.KeyNamespace, client.CleanseNamespace(b.App().Config.ActiveNamespace())) + ctx = context.WithValue(ctx, internal.KeyWithMetrics, b.app.factory.Client().HasMetrics()) return ctx } @@ -466,51 +528,68 @@ func (b *Browser) refreshActions() { if b.App().Content.Top() != nil && b.App().Content.Top().Name() != b.Name() { return } - aa := ui.KeyActions{ + aa := ui.NewKeyActionsFromMap(ui.KeyMap{ ui.KeyC: ui.NewKeyAction("Copy", b.cpCmd, false), tcell.KeyEnter: ui.NewKeyAction("View", b.enterCmd, false), tcell.KeyCtrlR: ui.NewKeyAction("Refresh", b.refreshCmd, false), - } + }) if b.app.ConOK() { b.namespaceActions(aa) if !b.app.Config.K9s.IsReadOnly() { if client.Can(b.meta.Verbs, "edit") { - aa[ui.KeyE] = ui.NewKeyAction("Edit", b.editCmd, true) + aa.Add(ui.KeyE, ui.NewKeyActionWithOpts("Edit", b.editCmd, + ui.ActionOpts{ + Visible: true, + Dangerous: true, + })) } if client.Can(b.meta.Verbs, "delete") { - aa[tcell.KeyCtrlD] = ui.NewKeyAction("Delete", b.deleteCmd, true) + aa.Add(tcell.KeyCtrlD, ui.NewKeyActionWithOpts("Delete", b.deleteCmd, + ui.ActionOpts{ + Visible: true, + Dangerous: true, + })) } + } else { + b.Actions().ClearDanger() } } - if !dao.IsK9sMeta(b.meta) { - aa[ui.KeyY] = ui.NewKeyAction("YAML", b.viewCmd, true) - aa[ui.KeyD] = ui.NewKeyAction("Describe", b.describeCmd, true) + aa.Add(ui.KeyY, ui.NewKeyAction(yamlAction, b.viewCmd, true)) + aa.Add(ui.KeyD, ui.NewKeyAction("Describe", b.describeCmd, true)) } - - pluginActions(b, aa) - hotKeyActions(b, aa) for _, f := range b.bindKeysFn { f(aa) } - b.Actions().Add(aa) + b.Actions().Merge(aa) + + if err := pluginActions(b, b.Actions()); err != nil { + log.Warn().Msgf("Plugins load failed: %s", err) + b.app.Logo().Warn("Plugins load failed!") + } + if err := hotKeyActions(b, b.Actions()); err != nil { + log.Warn().Msgf("Hotkeys load failed: %s", err) + b.app.Logo().Warn("HotKeys load failed!") + } b.app.Menu().HydrateMenu(b.Hints()) } -func (b *Browser) namespaceActions(aa ui.KeyActions) { +func (b *Browser) namespaceActions(aa *ui.KeyActions) { if !b.meta.Namespaced || b.GetTable().Path != "" { return } - b.namespaces = make(map[int]string, config.MaxFavoritesNS) - aa[ui.Key0] = ui.NewKeyAction(client.NamespaceAll, b.switchNamespaceCmd, true) + aa.Add(ui.KeyN, ui.NewKeyAction("Copy Namespace", b.cpNsCmd, false)) + + b.namespaces = make(map[int]string, data.MaxFavoritesNS) + aa.Add(ui.Key0, ui.NewKeyAction(client.NamespaceAll, b.switchNamespaceCmd, true)) b.namespaces[0] = client.NamespaceAll index := 1 for _, ns := range b.app.Config.FavNamespaces() { if ns == client.NamespaceAll { continue } - aa[ui.NumKeys[index]] = ui.NewKeyAction(ns, b.switchNamespaceCmd, true) + aa.Add(ui.NumKeys[index], ui.NewKeyAction(ns, b.switchNamespaceCmd, true)) b.namespaces[index] = ns index++ } @@ -520,7 +599,7 @@ func (b *Browser) simpleDelete(selections []string, msg string) { dialog.ShowConfirm(b.app.Styles.Dialog(), b.app.Content.Pages, "Confirm Delete", msg, func() { b.ShowDeleted() if len(selections) > 1 { - b.app.Flash().Infof("Delete %d marked %s", len(selections), b.GVR()) + b.app.Flash().Infof("Delete %d marked %s", len(selections), b.GVR().R()) } else { b.app.Flash().Infof("Delete resource %s %s", b.GVR(), selections[0]) } diff --git a/internal/view/cluster_info.go b/internal/view/cluster_info.go index c5a2cab7a6..3ff7cd839f 100644 --- a/internal/view/cluster_info.go +++ b/internal/view/cluster_info.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -50,7 +53,7 @@ func (c *ClusterInfo) StylesChanged(s *config.Styles) { func (c *ClusterInfo) hasMetrics() bool { mx := c.app.Conn().HasMetrics() if mx { - auth, err := c.app.Conn().CanI("", "metrics.k8s.io/v1beta1/nodes", client.ListAccess) + auth, err := c.app.Conn().CanI("", "metrics.k8s.io/v1beta1/nodes", "", client.ListAccess) if err != nil { log.Warn().Err(err).Msgf("No nodes metrics access") } @@ -97,6 +100,14 @@ func (c *ClusterInfo) ClusterInfoUpdated(data model.ClusterMeta) { c.ClusterInfoChanged(data, data) } +func (c *ClusterInfo) warnCell(s string, w bool) string { + if w { + return fmt.Sprintf("[orangered::b]%s", s) + } + + return s +} + // ClusterInfoChanged notifies the cluster meta was changed. func (c *ClusterInfo) ClusterInfoChanged(prev, curr model.ClusterMeta) { c.app.QueueUpdateDraw(func() { @@ -116,8 +127,8 @@ func (c *ClusterInfo) ClusterInfoChanged(prev, curr model.ClusterMeta) { _ = c.setCell(row, ui.AsPercDelta(prev.Mem, curr.Mem)) c.setDefCon(curr.Cpu, curr.Mem) } else { - row = c.setCell(row, "[orangered::b]n/a") - _ = c.setCell(row, "[orangered::b]n/a") + row = c.setCell(row, c.warnCell(render.NAValue, true)) + _ = c.setCell(row, c.warnCell(render.NAValue, true)) } c.updateStyle() }) diff --git a/internal/view/cm.go b/internal/view/cm.go index a0793c9ace..32d3e47c92 100644 --- a/internal/view/cm.go +++ b/internal/view/cm.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -25,17 +28,15 @@ func NewConfigMap(gvr client.GVR) ResourceViewer { return &s } -func (s *ConfigMap) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ - ui.KeyU: ui.NewKeyAction("UsedBy", s.refCmd, true), - }) +func (s *ConfigMap) bindKeys(aa *ui.KeyActions) { + aa.Add(ui.KeyU, ui.NewKeyAction("UsedBy", s.refCmd, true)) } func (s *ConfigMap) refCmd(evt *tcell.EventKey) *tcell.EventKey { - return scanRefs(evt, s.App(), s.GetTable(), "v1/configmaps") + return scanRefs(evt, s.App(), s.GetTable(), dao.CmGVR) } -func scanRefs(evt *tcell.EventKey, a *App, t *Table, gvr string) *tcell.EventKey { +func scanRefs(evt *tcell.EventKey, a *App, t *Table, gvr client.GVR) *tcell.EventKey { path := t.GetSelectedItem() if path == "" { return evt @@ -61,7 +62,7 @@ func scanRefs(evt *tcell.EventKey, a *App, t *Table, gvr string) *tcell.EventKey return nil } -func refContext(gvr, path string, wait bool) ContextFunc { +func refContext(gvr client.GVR, path string, wait bool) ContextFunc { return func(ctx context.Context) context.Context { ctx = context.WithValue(ctx, internal.KeyPath, path) ctx = context.WithValue(ctx, internal.KeyGVR, gvr) diff --git a/internal/view/cm_test.go b/internal/view/cm_test.go index 461ff4ba00..02781af46d 100644 --- a/internal/view/cm_test.go +++ b/internal/view/cm_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( diff --git a/internal/view/cmd/args.go b/internal/view/cmd/args.go new file mode 100644 index 0000000000..a594cf55ea --- /dev/null +++ b/internal/view/cmd/args.go @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package cmd + +import ( + "strings" +) + +const ( + nsKey = "ns" + topicKey = "topic" + filterKey = "filter" + fuzzyKey = "fuzzy" + labelKey = "labels" + contextKey = "context" +) + +type args map[string]string + +func newArgs(p *Interpreter, aa []string) args { + args := make(args, len(aa)) + if len(aa) == 0 { + return args + } + + for i := 0; i < len(aa); i++ { + a := strings.TrimSpace(aa[i]) + switch { + case strings.Index(a, contextFlag) == 0: + args[contextKey] = a[1:] + + case strings.Index(a, fuzzyFlag) == 0: + if a == fuzzyFlag { + if i++; i < len(aa) { + args[fuzzyKey] = strings.ToLower(strings.TrimSpace(aa[i])) + } + } else { + args[fuzzyKey] = strings.ToLower(a[2:]) + } + + case strings.Index(a, filterFlag) == 0: + args[filterKey] = strings.ToLower(a[1:]) + + case strings.Contains(a, labelFlag): + if ll := ToLabels(a); len(ll) != 0 { + args[labelKey] = strings.ToLower(a) + } + + default: + switch { + case p.IsContextCmd(): + args[contextKey] = a + case p.IsDirCmd(): + if _, ok := args[topicKey]; !ok { + args[topicKey] = a + } + case p.IsXrayCmd(): + if _, ok := args[topicKey]; ok { + args[nsKey] = strings.ToLower(a) + } else { + args[topicKey] = strings.ToLower(a) + } + default: + args[nsKey] = strings.ToLower(a) + } + } + } + + return args +} + +func (a args) hasFilters() bool { + _, fok := a[filterKey] + _, zok := a[fuzzyKey] + _, lok := a[labelKey] + + return fok || zok || lok +} diff --git a/internal/view/cmd/args_test.go b/internal/view/cmd/args_test.go new file mode 100644 index 0000000000..9c1a7e2a28 --- /dev/null +++ b/internal/view/cmd/args_test.go @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package cmd + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFlagsNew(t *testing.T) { + uu := map[string]struct { + i *Interpreter + aa []string + ll args + }{ + "empty": { + i: NewInterpreter("po"), + ll: make(args), + }, + "ns": { + i: NewInterpreter("po"), + aa: []string{"ns1"}, + ll: args{nsKey: "ns1"}, + }, + "ns+spaces": { + i: NewInterpreter("po"), + aa: []string{" ns1 "}, + ll: args{nsKey: "ns1"}, + }, + "filter": { + i: NewInterpreter("po"), + aa: []string{"/fred"}, + ll: args{filterKey: "fred"}, + }, + "inverse-filter": { + i: NewInterpreter("po"), + aa: []string{"/!fred"}, + ll: args{filterKey: "!fred"}, + }, + "fuzzy-filter": { + i: NewInterpreter("po"), + aa: []string{"-f", "fred"}, + ll: args{fuzzyKey: "fred"}, + }, + "fuzzy-filter-nospace": { + i: NewInterpreter("po"), + aa: []string{"-ffred"}, + ll: args{fuzzyKey: "fred"}, + }, + "filter+ns": { + i: NewInterpreter("po"), + aa: []string{"/fred", " ns1 "}, + ll: args{nsKey: "ns1", filterKey: "fred"}, + }, + "label": { + i: NewInterpreter("po"), + aa: []string{"app=fred"}, + ll: args{labelKey: "app=fred"}, + }, + "label-toast": { + i: NewInterpreter("po"), + aa: []string{"="}, + ll: make(args), + }, + "multi-labels": { + i: NewInterpreter("po"), + aa: []string{"app=fred,blee=duh"}, + ll: args{labelKey: "app=fred,blee=duh"}, + }, + "label+ns": { + i: NewInterpreter("po"), + aa: []string{"a=b,c=d", " ns1 "}, + ll: args{labelKey: "a=b,c=d", nsKey: "ns1"}, + }, + "full-monty": { + i: NewInterpreter("po"), + aa: []string{"app=fred", "ns1", "-f", "blee", "/zorg"}, + ll: args{ + filterKey: "zorg", + fuzzyKey: "blee", + labelKey: "app=fred", + nsKey: "ns1", + }, + }, + "full-monty+ctx": { + i: NewInterpreter("po"), + aa: []string{"app=fred", "ns1", "-f", "blee", "/zorg", "@ctx1"}, + ll: args{ + filterKey: "zorg", + fuzzyKey: "blee", + labelKey: "app=fred", + nsKey: "ns1", + contextKey: "ctx1", + }, + }, + "caps": { + i: NewInterpreter("po"), + aa: []string{"app=fred", "ns1", "-f", "blee", "/zorg", "@Dev"}, + ll: args{ + filterKey: "zorg", + fuzzyKey: "blee", + labelKey: "app=fred", + nsKey: "ns1", + contextKey: "Dev"}, + }, + "ctx": { + i: NewInterpreter("ctx"), + aa: []string{"Dev"}, + ll: args{contextKey: "Dev"}, + }, + "bork": { + i: NewInterpreter("apply -f"), + ll: args{}, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + l := newArgs(u.i, u.aa) + assert.Equal(t, len(u.ll), len(l)) + assert.Equal(t, u.ll, l) + }) + } +} + +func TestFlagsHasFilters(t *testing.T) { + uu := map[string]struct { + i *Interpreter + aa []string + ok bool + }{ + "empty": {}, + "ns": { + i: NewInterpreter("po"), + aa: []string{"ns1"}, + }, + "filter": { + i: NewInterpreter("po"), + aa: []string{"/fred"}, + ok: true, + }, + "inverse-filter": { + i: NewInterpreter("po"), + aa: []string{"/!fred"}, + ok: true, + }, + "fuzzy-filter": { + i: NewInterpreter("po"), + aa: []string{"-f", "fred"}, + ok: true, + }, + "filter+ns": { + i: NewInterpreter("po"), + aa: []string{"/fred", "ns1"}, + ok: true, + }, + "label": { + i: NewInterpreter("po"), + aa: []string{"app=fred"}, + ok: true, + }, + "multi-labels": { + i: NewInterpreter("po"), + aa: []string{"app=fred,blee=duh"}, + ok: true, + }, + "label+ns": { + i: NewInterpreter("po"), + aa: []string{"app=fred", "ns1"}, + ok: true, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + l := newArgs(u.i, u.aa) + assert.Equal(t, u.ok, l.hasFilters()) + }) + } +} diff --git a/internal/view/cmd/helpers.go b/internal/view/cmd/helpers.go new file mode 100644 index 0000000000..9ceec0dae8 --- /dev/null +++ b/internal/view/cmd/helpers.go @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package cmd + +import ( + "slices" + "strings" + + "github.com/derailed/k9s/internal/client" +) + +func ToLabels(s string) map[string]string { + var ( + ll = strings.Split(s, ",") + lbls = make(map[string]string, len(ll)) + ) + for _, l := range ll { + kv := strings.Split(l, "=") + if len(kv) < 2 || kv[0] == "" || kv[1] == "" { + continue + } + lbls[kv[0]] = kv[1] + } + if len(lbls) == 0 { + return nil + } + + return lbls +} + +// ShouldAddSuggest checks if a suggestion match the given command. +func ShouldAddSuggest(command, suggest string) (string, bool) { + if command != suggest && strings.HasPrefix(suggest, command) { + return strings.TrimPrefix(suggest, command), true + } + + return "", false +} + +// SuggestSubCommand suggests namespaces or contexts based on current command. +func SuggestSubCommand(command string, namespaces client.NamespaceNames, contexts []string) []string { + p := NewInterpreter(command) + var suggests []string + switch { + case p.IsCowCmd(): + fallthrough + case p.IsHelpCmd(): + fallthrough + case p.IsAliasCmd(): + fallthrough + case p.IsBailCmd(): + fallthrough + case p.IsDirCmd(): + fallthrough + case p.IsAliasCmd(): + return nil + + case p.IsXrayCmd(): + _, ns, ok := p.XrayArgs() + if !ok || ns == "" { + return nil + } + suggests = completeNS(ns, namespaces) + + case p.IsContextCmd(): + n, ok := p.ContextArg() + if !ok { + return nil + } + suggests = completeCtx(n, contexts) + + case p.HasNS(): + if n, ok := p.HasContext(); ok { + suggests = completeCtx(n, contexts) + } + if len(suggests) > 0 { + break + } + + ns, ok := p.NSArg() + if !ok { + return nil + } + suggests = completeNS(ns, namespaces) + + default: + if n, ok := p.HasContext(); ok { + suggests = completeCtx(n, contexts) + } + } + slices.Sort(suggests) + + return suggests +} + +func completeNS(s string, nn client.NamespaceNames) []string { + s = strings.ToLower(s) + var suggests []string + if suggest, ok := ShouldAddSuggest(s, client.NamespaceAll); ok { + suggests = append(suggests, suggest) + } + for ns := range nn { + if suggest, ok := ShouldAddSuggest(s, ns); ok { + suggests = append(suggests, suggest) + } + } + + return suggests +} + +func completeCtx(s string, cc []string) []string { + var suggests []string + for _, ctxName := range cc { + if suggest, ok := ShouldAddSuggest(s, ctxName); ok { + suggests = append(suggests, suggest) + } + } + + return suggests +} diff --git a/internal/view/cmd/helpers_test.go b/internal/view/cmd/helpers_test.go new file mode 100644 index 0000000000..da4f821d00 --- /dev/null +++ b/internal/view/cmd/helpers_test.go @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package cmd + +import ( + "testing" + + "github.com/rs/zerolog" + "github.com/stretchr/testify/assert" +) + +func init() { + zerolog.SetGlobalLevel(zerolog.FatalLevel) +} + +func Test_toLabels(t *testing.T) { + uu := map[string]struct { + s string + ll map[string]string + }{ + "empty": {}, + "toast": { + s: "=", + }, + "toast-1": { + s: "=,", + }, + "toast-2": { + s: ",", + }, + "toast-3": { + s: ",=", + }, + "simple": { + s: "a=b", + ll: map[string]string{"a": "b"}, + }, + "multi": { + s: "a=b,c=d", + ll: map[string]string{"a": "b", "c": "d"}, + }, + "multi-toast1": { + s: "a=,c=d", + ll: map[string]string{"c": "d"}, + }, + "multi-toast2": { + s: "a=b,=d", + ll: map[string]string{"a": "b"}, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.ll, ToLabels(u.s)) + }) + } +} + +func TestSuggestSubCommand(t *testing.T) { + namespaceNames := map[string]struct{}{ + "kube-system": {}, + "kube-public": {}, + "default": {}, + "nginx-ingress": {}, + } + contextNames := []string{"develop", "test", "pre", "prod"} + + tests := []struct { + Command string + Suggestions []string + }{ + {Command: "q", Suggestions: nil}, + {Command: "xray dp", Suggestions: nil}, + {Command: "help k", Suggestions: nil}, + {Command: "ctx p", Suggestions: []string{"re", "rod"}}, + {Command: "ctx p", Suggestions: []string{"re", "rod"}}, + {Command: "ctx pr", Suggestions: []string{"e", "od"}}, + {Command: "context d", Suggestions: []string{"evelop"}}, + {Command: "contexts t", Suggestions: []string{"est"}}, + {Command: "po ", Suggestions: nil}, + {Command: "po x", Suggestions: nil}, + {Command: "po k", Suggestions: []string{"ube-public", "ube-system"}}, + {Command: "po kube-", Suggestions: []string{"public", "system"}}, + } + + for _, tt := range tests { + got := SuggestSubCommand(tt.Command, namespaceNames, contextNames) + assert.Equal(t, tt.Suggestions, got) + } +} diff --git a/internal/view/cmd/interpreter.go b/internal/view/cmd/interpreter.go new file mode 100644 index 0000000000..d7af1bafd2 --- /dev/null +++ b/internal/view/cmd/interpreter.go @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package cmd + +import ( + "strings" +) + +// Interpreter tracks user prompt input. +type Interpreter struct { + line string + cmd string + args args +} + +// NewInterpreter returns a new instance. +func NewInterpreter(s string) *Interpreter { + c := Interpreter{ + line: s, + args: make(args), + } + c.grok() + + return &c +} + +func (c *Interpreter) grok() { + ff := strings.Fields(c.line) + if len(ff) == 0 { + return + } + c.cmd = strings.ToLower(ff[0]) + c.args = newArgs(c, ff[1:]) +} + +// HasNS returns true if ns is present in prompt. +func (c *Interpreter) HasNS() bool { + ns, ok := c.args[nsKey] + + return ok && ns != "" +} + +// Cmd returns the command. +func (c *Interpreter) Cmd() string { + return c.cmd +} + +// IsBlank returns true if prompt is empty. +func (c *Interpreter) IsBlank() bool { + return c.line == "" +} + +// Amend merges prompts. +func (c *Interpreter) Amend(c1 *Interpreter) { + c.cmd = c1.cmd + if c.args == nil { + c.args = make(args, len(c1.args)) + } + for k, v := range c1.args { + if v != "" { + c.args[k] = v + } + } +} + +// Reset resets with new command. +func (c *Interpreter) Reset(s string) *Interpreter { + c.line = s + c.grok() + + return c +} + +// GetLine teturns the prompt. +func (c *Interpreter) GetLine() string { + return strings.TrimSpace(c.line) +} + +// IsCowCmd returns true if cow cmd is detected. +func (c *Interpreter) IsCowCmd() bool { + return c.cmd == cowCmd +} + +// IsHelpCmd returns true if help cmd is detected. +func (c *Interpreter) IsHelpCmd() bool { + _, ok := helpCmd[c.cmd] + return ok +} + +// IsBailCmd returns true if quit cmd is detected. +func (c *Interpreter) IsBailCmd() bool { + _, ok := bailCmd[c.cmd] + return ok +} + +// IsAliasCmd returns true if alias cmd is detected. +func (c *Interpreter) IsAliasCmd() bool { + _, ok := aliasCmd[c.cmd] + return ok +} + +// IsXrayCmd returns true if xray cmd is detected. +func (c *Interpreter) IsXrayCmd() bool { + _, ok := xrayCmd[c.cmd] + + return ok +} + +// IsContextCmd returns true if context cmd is detected. +func (c *Interpreter) IsContextCmd() bool { + _, ok := contextCmd[c.cmd] + + return ok +} + +// IsNamespaceCmd returns true if ns cmd is detected. +func (c *Interpreter) IsNamespaceCmd() bool { + _, ok := namespaceCmd[c.cmd] + + return ok +} + +// IsDirCmd returns true if dir cmd is detected. +func (c *Interpreter) IsDirCmd() bool { + _, ok := dirCmd[c.cmd] + return ok +} + +// IsRBACCmd returns true if rbac cmd is detected. +func (c *Interpreter) IsRBACCmd() bool { + return c.cmd == canCmd +} + +// ContextArg returns context cmd arg. +func (c *Interpreter) ContextArg() (string, bool) { + if !c.IsContextCmd() { + return "", false + } + + return c.args[contextKey], true +} + +// ResetContextArg deletes context arg. +func (c *Interpreter) ResetContextArg() { + delete(c.args, contextFlag) +} + +// DirArg returns the directory is present. +func (c *Interpreter) DirArg() (string, bool) { + if !c.IsDirCmd() { + return "", false + } + d, ok := c.args[topicKey] + + return d, ok && d != "" +} + +// CowArg returns the cow message. +func (c *Interpreter) CowArg() (string, bool) { + if !c.IsCowCmd() { + return "", false + } + m, ok := c.args[nsKey] + + return m, ok && m != "" +} + +// RBACArgs returns the subject and topic is any. +func (c *Interpreter) RBACArgs() (string, string, bool) { + if !c.IsRBACCmd() { + return "", "", false + } + tt := rbacRX.FindStringSubmatch(c.line) + if len(tt) < 3 { + return "", "", false + } + + return tt[1], tt[2], true +} + +// XRayArgs return the gvr and ns if any. +func (c *Interpreter) XrayArgs() (string, string, bool) { + if !c.IsXrayCmd() { + return "", "", false + } + gvr, ok1 := c.args[topicKey] + if !ok1 { + return "", "", false + } + + ns, ok2 := c.args[nsKey] + switch { + case ok1 && ok2: + return gvr, ns, true + case ok1 && !ok2: + return gvr, "", true + default: + return "", "", false + } +} + +// FilterArg returns the current filter if any. +func (c *Interpreter) FilterArg() (string, bool) { + f, ok := c.args[filterKey] + + return f, ok && f != "" +} + +// FuzzyArg returns the fuzzy filter if any. +func (c *Interpreter) FuzzyArg() (string, bool) { + f, ok := c.args[fuzzyKey] + + return f, ok && f != "" +} + +// NSArg returns the current ns if any. +func (c *Interpreter) NSArg() (string, bool) { + ns, ok := c.args[nsKey] + + return ns, ok && ns != "" +} + +// HasContext returns the current context if any. +func (c *Interpreter) HasContext() (string, bool) { + ctx, ok := c.args[contextKey] + + return ctx, ok && ctx != "" +} + +// LabelsArg return the labels map if any. +func (c *Interpreter) LabelsArg() (map[string]string, bool) { + ll, ok := c.args[labelKey] + + return ToLabels(ll), ok +} diff --git a/internal/view/cmd/interpreter_test.go b/internal/view/cmd/interpreter_test.go new file mode 100644 index 0000000000..7266043dff --- /dev/null +++ b/internal/view/cmd/interpreter_test.go @@ -0,0 +1,470 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package cmd_test + +import ( + "testing" + + "github.com/derailed/k9s/internal/view/cmd" + "github.com/stretchr/testify/assert" +) + +func TestRbacCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + args []string + }{ + "empty": {}, + "user": { + cmd: "can u:fernand", + ok: true, + args: []string{"u", "fernand"}, + }, + "user_spacing": { + cmd: "can u: fernand ", + ok: true, + args: []string{"u", "fernand"}, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + assert.Equal(t, u.ok, p.IsRBACCmd()) + + c, s, ok := p.RBACArgs() + assert.Equal(t, u.ok, ok) + if u.ok { + assert.Equal(t, u.args[0], c) + assert.Equal(t, u.args[1], s) + } + }) + } +} + +func TestNsCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + ns string + }{ + "empty": {}, + "happy": { + cmd: "pod fred", + ok: true, + ns: "fred", + }, + "ns-arg-spaced": { + cmd: "pod fred ", + ok: true, + ns: "fred", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + ns, ok := p.NSArg() + assert.Equal(t, u.ok, ok) + if u.ok { + assert.Equal(t, u.ns, ns) + } + }) + } +} + +func TestFilterCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + filter string + }{ + "empty": {}, + "normal": { + cmd: "pod /fred", + ok: true, + filter: "fred", + }, + "caps": { + cmd: "POD /FRED", + ok: true, + filter: "fred", + }, + "filter+ns": { + cmd: "pod /fred ns1", + ok: true, + filter: "fred", + }, + "ns+filter": { + cmd: "pod ns1 /fred", + ok: true, + filter: "fred", + }, + "ns+filter+labels": { + cmd: "pod ns1 /fred app=blee,fred=zorg", + ok: true, + filter: "fred", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + f, ok := p.FilterArg() + assert.Equal(t, u.ok, ok) + if u.ok { + assert.Equal(t, u.filter, f) + } + }) + } +} + +func TestLabelCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + labels map[string]string + }{ + "empty": {}, + "plain": { + cmd: "pod fred=blee", + ok: true, + labels: map[string]string{"fred": "blee"}, + }, + "multi": { + cmd: "pod fred=blee,zorg=duh", + ok: true, + labels: map[string]string{"fred": "blee", "zorg": "duh"}, + }, + "multi-ns": { + cmd: "pod fred=blee,zorg=duh ns1", + ok: true, + labels: map[string]string{"fred": "blee", "zorg": "duh"}, + }, + "l-arg-spaced": { + cmd: "pod fred=blee ", + ok: true, + labels: map[string]string{"fred": "blee"}, + }, + "l-arg-caps": { + cmd: "POD FRED=BLEE ", + ok: true, + labels: map[string]string{"fred": "blee"}, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + ll, ok := p.LabelsArg() + assert.Equal(t, u.ok, ok) + if u.ok { + assert.Equal(t, u.labels, ll) + } + }) + } +} + +func TestXRayCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + res, ns string + }{ + "empty": {}, + + "happy": { + cmd: "xray po", + ok: true, + res: "po", + }, + + "happy+ns": { + cmd: "xray po ns1", + ok: true, + res: "po", + ns: "ns1", + }, + + "toast": { + cmd: "xrayzor po", + }, + + "toast-1": { + cmd: "xray", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + res, ns, ok := p.XrayArgs() + assert.Equal(t, u.ok, ok) + if u.ok { + assert.Equal(t, u.res, res) + assert.Equal(t, u.ns, ns) + } + }) + } +} + +func TestDirCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + dir string + }{ + "empty": {}, + + "happy": { + cmd: "dir dir1", + ok: true, + dir: "dir1", + }, + + "extra-ns": { + cmd: "dir dir1 ns1", + ok: true, + dir: "dir1", + }, + + "toast": { + cmd: "dirdel dir1", + }, + + "toast-nodir": { + cmd: "dir", + }, + "caps": { + cmd: "dir DirName", + ok: true, + dir: "DirName", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + dir, ok := p.DirArg() + assert.Equal(t, u.ok, ok) + assert.Equal(t, u.dir, dir) + }) + } +} + +func TestRBACCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + cat, sub string + }{ + "empty": {}, + "toast": { + cmd: "canopy u:bozo", + }, + "toast-1": { + cmd: "can u:", + }, + "toast-2": { + cmd: "can bozo", + }, + "user": { + cmd: "can u:bozo", + ok: true, + cat: "u", + sub: "bozo", + }, + "group": { + cmd: "can g:bozo", + ok: true, + cat: "g", + sub: "bozo", + }, + "sa": { + cmd: "can s:bozo", + ok: true, + cat: "s", + sub: "bozo", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + cat, sub, ok := p.RBACArgs() + assert.Equal(t, u.ok, ok) + if u.ok { + assert.Equal(t, u.cat, cat) + assert.Equal(t, u.sub, sub) + } + }) + } +} + +func TestContextCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + ctx string + }{ + "empty": {}, + "happy-full": { + cmd: "context ctx1", + ok: true, + ctx: "ctx1", + }, + "happy-alias": { + cmd: "ctx ctx1", + ok: true, + ctx: "ctx1", + }, + "toast": { + cmd: "ctxto ctx1", + }, + "caps": { + cmd: "ctx Dev", + ok: true, + ctx: "Dev", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + assert.Equal(t, u.ok, p.IsContextCmd()) + if u.ok { + ctx, ok := p.ContextArg() + assert.Equal(t, u.ok, ok) + assert.Equal(t, u.ctx, ctx) + } + }) + } +} + +func TestHelpCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + }{ + "empty": {}, + "plain": { + cmd: "help", + ok: true, + }, + "toast": { + cmd: "helpme", + }, + "toast1": { + cmd: "hozer", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + assert.Equal(t, u.ok, p.IsHelpCmd()) + }) + } +} + +func TestBailCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + }{ + "empty": {}, + "plain": { + cmd: "quit", + ok: true, + }, + "q": { + cmd: "q", + ok: true, + }, + "q!": { + cmd: "q!", + ok: true, + }, + "toast": { + cmd: "zorg", + }, + "toast1": { + cmd: "quitter", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + assert.Equal(t, u.ok, p.IsBailCmd()) + }) + } +} + +func TestAliasCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + }{ + "empty": {}, + "plain": { + cmd: "alias", + ok: true, + }, + "a": { + cmd: "a", + ok: true, + }, + "toast": { + cmd: "abba", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + assert.Equal(t, u.ok, p.IsAliasCmd()) + }) + } +} + +func TestCowCmd(t *testing.T) { + uu := map[string]struct { + cmd string + ok bool + }{ + "empty": {}, + "plain": { + cmd: "cow", + ok: true, + }, + "msg": { + cmd: "cow bumblebeetuna", + ok: true, + }, + "toast": { + cmd: "cowdy", + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + p := cmd.NewInterpreter(u.cmd) + assert.Equal(t, u.ok, p.IsCowCmd()) + }) + } +} diff --git a/internal/view/cmd/types.go b/internal/view/cmd/types.go new file mode 100644 index 0000000000..3ea52771d5 --- /dev/null +++ b/internal/view/cmd/types.go @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package cmd + +import "regexp" + +const ( + cowCmd = "cow" + canCmd = "can" + nsFlag = "-n" + filterFlag = "/" + labelFlag = "=" + fuzzyFlag = "-f" + contextFlag = "@" +) + +var ( + rbacRX = regexp.MustCompile(`^can\s+([u|g|s]):\s*([\w-:]+)\s*$`) + + contextCmd = map[string]struct{}{ + "ctx": {}, + "context": {}, + "contexts": {}, + } + namespaceCmd = map[string]struct{}{ + "ns": {}, + "namespace": {}, + "namespaces": {}, + } + dirCmd = map[string]struct{}{ + "dir": {}, + "d": {}, + "ls": {}, + } + bailCmd = map[string]struct{}{ + "q": {}, + "q!": {}, + "qa": {}, + "Q": {}, + "quit": {}, + "exit": {}, + } + helpCmd = map[string]struct{}{ + "?": {}, + "h": {}, + "help": {}, + } + aliasCmd = map[string]struct{}{ + "a": {}, + "alias": {}, + } + xrayCmd = map[string]struct{}{ + "x": {}, + "xr": {}, + "xray": {}, + } +) diff --git a/internal/view/command.go b/internal/view/command.go index 1cc473fb5c..befa0b8dca 100644 --- a/internal/view/command.go +++ b/internal/view/command.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -11,19 +14,18 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/view/cmd" "github.com/rs/zerolog/log" ) var ( customViewers MetaViewers - - canRX = regexp.MustCompile(`\Acan\s([u|g|s]):([\w-:]+)\b`) + contextRX = regexp.MustCompile(`\s+@([\w-]+)`) ) // Command represents a user command. type Command struct { - app *App - + app *App alias *dao.Alias mx sync.Mutex } @@ -35,11 +37,16 @@ func NewCommand(app *App) *Command { } } +// AliasesFor gather all known aliases for a given resource. +func (c *Command) AliasesFor(s string) []string { + return c.alias.AliasesFor(s) +} + // Init initializes the command. -func (c *Command) Init() error { +func (c *Command) Init(path string) error { c.alias = dao.NewAlias(c.app.factory) - if _, err := c.alias.Ensure(); err != nil { - log.Error().Err(err).Msgf("command init failed!") + if _, err := c.alias.Ensure(path); err != nil { + log.Error().Err(err).Msgf("Alias ensure failed!") return err } customViewers = loadCustomViewers() @@ -48,14 +55,14 @@ func (c *Command) Init() error { } // Reset resets Command and reload aliases. -func (c *Command) Reset(clear bool) error { +func (c *Command) Reset(path string, clear bool) error { c.mx.Lock() defer c.mx.Unlock() if clear { c.alias.Clear() } - if _, err := c.alias.Ensure(); err != nil { + if _, err := c.alias.Ensure(path); err != nil { return err } @@ -63,184 +70,239 @@ func (c *Command) Reset(clear bool) error { } func allowedXRay(gvr client.GVR) bool { - gg := []string{ - "v1/pods", - "v1/services", - "apps/v1/deployments", - "apps/v1/daemonsets", - "apps/v1/statefulsets", - "apps/v1/replicasets", - } - for _, g := range gg { - if g == gvr.String() { - return true - } + gg := map[string]struct{}{ + "v1/pods": {}, + "v1/services": {}, + "apps/v1/deployments": {}, + "apps/v1/daemonsets": {}, + "apps/v1/statefulsets": {}, + "apps/v1/replicasets": {}, + } + _, ok := gg[gvr.String()] + + return ok +} + +func (c *Command) contextCmd(p *cmd.Interpreter) error { + ct, ok := p.ContextArg() + if !ok { + return fmt.Errorf("invalid command use `context xxx`") + } + + if ct != "" { + return useContext(c.app, ct) + } + + gvr, v, err := c.viewMetaFor(p) + if err != nil { + return err + } + + return c.exec(p, gvr, c.componentFor(gvr, ct, v), true) +} + +func (c *Command) namespaceCmd(p *cmd.Interpreter) bool { + ns, ok := p.NSArg() + if !ok { + return false + } + + if ns != "" { + _ = p.Reset("pod " + ns) } return false } -func (c *Command) xrayCmd(cmd string) error { - tokens := strings.Split(cmd, " ") - if len(tokens) < 2 { - return errors.New("you must specify a resource") +func (c *Command) aliasCmd(p *cmd.Interpreter) error { + filter, _ := p.FilterArg() + + gvr := client.NewGVR("aliases") + v := NewAlias(gvr) + v.SetFilter(filter) + + return c.exec(p, gvr, v, false) +} + +func (c *Command) xrayCmd(p *cmd.Interpreter) error { + arg, cns, ok := p.XrayArgs() + if !ok { + return errors.New("invalid command. use `xray xxx`") } - gvr, ok := c.alias.AsGVR(tokens[1]) + gvr, _, ok := c.alias.AsGVR(arg) if !ok { - return fmt.Errorf("`%s` command not found", cmd) + return fmt.Errorf("invalid resource name: %q", arg) } if !allowedXRay(gvr) { - return fmt.Errorf("`%s` command not found", cmd) + return fmt.Errorf("unsupported resource %q", arg) } - - x := NewXray(gvr) ns := c.app.Config.ActiveNamespace() - if len(tokens) == 3 { - ns = tokens[2] + if cns != "" { + ns = cns } if err := c.app.Config.SetActiveNamespace(client.CleanseNamespace(ns)); err != nil { return err } - if err := c.app.Config.Save(); err != nil { + if err := c.app.switchNS(ns); err != nil { return err } - return c.exec(cmd, "xrays", x, true) + return c.exec(p, client.NewGVR("xrays"), NewXray(gvr), true) } // Run execs the command by showing associated display. -func (c *Command) run(cmd, path string, clearStack bool) error { - if c.specialCmd(cmd, path) { +func (c *Command) run(p *cmd.Interpreter, fqn string, clearStack bool) error { + if c.specialCmd(p) { return nil } - cmds := strings.Split(cmd, " ") - command := strings.ToLower(cmds[0]) - gvr, v, err := c.viewMetaFor(command) + gvr, v, err := c.viewMetaFor(p) if err != nil { return err } - var cns string - tt := strings.Split(gvr, " ") - if len(tt) == 2 { - gvr, cns = tt[0], tt[1] + + ns := c.app.Config.ActiveNamespace() + if cns, ok := p.NSArg(); ok { + ns = cns + } + if err := c.app.switchNS(ns); err != nil { + return err } - switch command { - case "ctx", "context", "contexts": - if len(cmds) == 2 { - return useContext(c.app, cmds[1]) - } - return c.exec(cmd, gvr, c.componentFor(gvr, path, v), clearStack) - case "dir": - if len(cmds) != 2 { - return errors.New("you must specify a directory") + if context, ok := p.HasContext(); ok { + if context != c.app.Config.ActiveContextName() { + if err := c.app.Config.Save(true); err != nil { + log.Error().Err(err).Msg("config save failed!") + } else { + log.Debug().Msgf("Saved context config for: %q", context) + } } - return c.app.dirCmd(cmds[1]) - default: - // checks if Command includes a namespace - ns := c.app.Config.ActiveNamespace() - if len(cmds) == 2 { - ns = cmds[1] + res, err := dao.AccessorFor(c.app.factory, client.NewGVR("contexts")) + if err != nil { + return err } - if cns != "" { - ns = cns + switcher, ok := res.(dao.Switchable) + if !ok { + return errors.New("expecting a switchable resource") } - if err := c.app.switchNS(ns); err != nil { + if err := switcher.Switch(context); err != nil { + log.Error().Err(err).Msgf("Context switch failed") return err } - if !c.alias.Check(command) { - return fmt.Errorf("`%s` Command not found", cmd) + if err := c.app.switchContext(p, false); err != nil { + return err } - return c.exec(cmd, gvr, c.componentFor(gvr, path, v), clearStack) } + + co := c.componentFor(gvr, fqn, v) + co.SetFilter("") + co.SetLabelFilter(nil) + if f, ok := p.FilterArg(); ok { + co.SetFilter(f) + } + if f, ok := p.FuzzyArg(); ok { + co.SetFilter("-f " + f) + } + if ll, ok := p.LabelsArg(); ok { + co.SetLabelFilter(ll) + } + + return c.exec(p, gvr, co, clearStack) } func (c *Command) defaultCmd() error { if c.app.Conn() == nil || !c.app.Conn().ConnectionOK() { - return c.run("context", "", true) + return c.run(cmd.NewInterpreter("context"), "", true) } - view := c.app.Config.ActiveView() - if view == "" { - return c.run("pod", "", true) - } - tokens := strings.Split(view, " ") - cmd := view - if len(tokens) == 1 { - if !isContextCmd(tokens[0]) { - cmd = tokens[0] + " " + c.app.Config.ActiveNamespace() - } + + p := cmd.NewInterpreter(c.app.Config.ActiveView()) + if p.IsBlank() { + return c.run(p.Reset("pod"), "", true) } - if err := c.run(cmd, "", true); err != nil { - log.Error().Err(err).Msgf("Default run command failed %q", cmd) - return c.run("pod", "", true) + if err := c.run(p, "", true); err != nil { + log.Error().Err(err).Msgf("Default run command failed %q", p.GetLine()) + return c.run(p.Reset("pod"), "", true) } - return nil -} -func isContextCmd(c string) bool { - return c == "ctx" || c == "context" + return nil } -func (c *Command) specialCmd(cmd, path string) bool { - cmds := strings.Split(cmd, " ") - switch cmds[0] { - case "cow": - c.app.cowCmd(path) - return true - case "q", "q!", "qa", "Q", "quit": +func (c *Command) specialCmd(p *cmd.Interpreter) bool { + switch { + case p.IsCowCmd(): + if msg, ok := p.CowArg(); !ok { + c.app.Flash().Errf("Invalid command. Use `cow xxx`") + } else { + c.app.cowCmd(msg) + } + case p.IsBailCmd(): c.app.BailOut() - return true - case "?", "h", "help": - c.app.helpCmd(nil) - return true - case "a", "alias": - c.app.aliasCmd(nil) - return true - case "x", "xray": - if err := c.xrayCmd(cmd); err != nil { + case p.IsHelpCmd(): + _ = c.app.helpCmd(nil) + case p.IsAliasCmd(): + if err := c.aliasCmd(p); err != nil { c.app.Flash().Err(err) } - return true - default: - if !canRX.MatchString(cmd) { - return false + case p.IsXrayCmd(): + if err := c.xrayCmd(p); err != nil { + c.app.Flash().Err(err) } - tokens := canRX.FindAllStringSubmatch(cmd, -1) - if len(tokens) == 1 && len(tokens[0]) == 3 { - if err := c.app.inject(NewPolicy(c.app, tokens[0][1], tokens[0][2]), false); err != nil { - log.Error().Err(err).Msgf("policy view load failed") - return false - } - return true + case p.IsRBACCmd(): + if cat, sub, ok := p.RBACArgs(); !ok { + c.app.Flash().Errf("Invalid command. Use `can [u|g|s]:xxx`") + } else if err := c.app.inject(NewPolicy(c.app, cat, sub), true); err != nil { + c.app.Flash().Err(err) + } + case p.IsContextCmd(): + if err := c.contextCmd(p); err != nil { + c.app.Flash().Err(err) + } + case p.IsNamespaceCmd(): + return c.namespaceCmd(p) + case p.IsDirCmd(): + if a, ok := p.DirArg(); !ok { + c.app.Flash().Errf("Invalid command. Use `dir xxx`") + } else if err := c.app.dirCmd(a); err != nil { + c.app.Flash().Err(err) } + default: + return false } - return false + + return true } -func (c *Command) viewMetaFor(cmd string) (string, *MetaViewer, error) { - gvr, ok := c.alias.AsGVR(cmd) +func (c *Command) viewMetaFor(p *cmd.Interpreter) (client.GVR, *MetaViewer, error) { + agvr, exp, ok := c.alias.AsGVR(p.Cmd()) if !ok { - return "", nil, fmt.Errorf("`%s` command not found", cmd) + return client.NoGVR, nil, fmt.Errorf("`%s` command not found", p.Cmd()) + } + gvr := agvr + if exp != "" { + ff := strings.Fields(exp) + ff[0] = agvr.String() + ap := cmd.NewInterpreter(strings.Join(ff, " ")) + gvr = client.NewGVR(ap.Cmd()) + p.Amend(ap) } - v, ok := customViewers[gvr] - if !ok { - return gvr.String(), &MetaViewer{viewerFn: NewBrowser}, nil + v := MetaViewer{viewerFn: NewBrowser} + if mv, ok := customViewers[gvr]; ok { + v = mv } - return gvr.String(), &v, nil + return gvr, &v, nil } -func (c *Command) componentFor(gvr, path string, v *MetaViewer) ResourceViewer { +func (c *Command) componentFor(gvr client.GVR, fqn string, v *MetaViewer) ResourceViewer { var view ResourceViewer if v.viewerFn != nil { - view = v.viewerFn(client.NewGVR(gvr)) + view = v.viewerFn(gvr) } else { - view = NewBrowser(client.NewGVR(gvr)) + view = NewBrowser(gvr) } - view.SetInstance(path) + view.SetInstance(fqn) if v.enterFn != nil { view.GetTable().SetEnterFn(v.enterFn) } @@ -248,7 +310,7 @@ func (c *Command) componentFor(gvr, path string, v *MetaViewer) ResourceViewer { return view } -func (c *Command) exec(cmd, gvr string, comp model.Component, clearStack bool) (err error) { +func (c *Command) exec(p *cmd.Interpreter, gvr client.GVR, comp model.Component, clearStack bool) (err error) { defer func() { if e := recover(); e != nil { log.Error().Msgf("Something bad happened! %#v", e) @@ -256,32 +318,27 @@ func (c *Command) exec(cmd, gvr string, comp model.Component, clearStack bool) ( log.Debug().Msgf("History %v", c.app.cmdHistory.List()) log.Error().Msg(string(debug.Stack())) - hh := c.app.cmdHistory.List() - if len(hh) == 0 { - _ = c.run("pod", "", true) - } else { - _ = c.run(hh[0], "", true) + p := cmd.NewInterpreter("pod") + if cmd := c.app.cmdHistory.Pop(); cmd != "" { + p = p.Reset(cmd) } - err = fmt.Errorf("invalid command %q", cmd) + err = c.run(p, "", true) } }() if comp == nil { return fmt.Errorf("no component found for %s", gvr) } - c.app.Flash().Infof("Viewing %s...", client.NewGVR(gvr).R()) - if tokens := strings.Split(cmd, " "); len(tokens) >= 2 { - cmd = tokens[0] - } - c.app.Config.SetActiveView(cmd) - if err := c.app.Config.Save(); err != nil { - log.Error().Err(err).Msg("Config save failed!") + c.app.Flash().Infof("Viewing %s...", gvr.R()) + if clearStack { + cmd := contextRX.ReplaceAllString(p.GetLine(), "") + c.app.Config.SetActiveView(cmd) } if err := c.app.inject(comp, clearStack); err != nil { return err } - c.app.cmdHistory.Push(cmd) + c.app.cmdHistory.Push(p.GetLine()) return } diff --git a/internal/view/container.go b/internal/view/container.go index 7bc0501dbe..e2793c0eab 100644 --- a/internal/view/container.go +++ b/internal/view/container.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -8,11 +11,11 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/port" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" - "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" ) @@ -36,52 +39,65 @@ func NewContainer(gvr client.GVR) ResourceViewer { return &c } -func (c *Container) portForwardIndicator(data *render.TableData) { +func (c *Container) portForwardIndicator(data *model1.TableData) { ff := c.App().factory.Forwarders() - col := data.IndexOfHeader("PF") - for _, re := range data.RowEvents { + col, ok := data.IndexOfHeader("PF") + if !ok { + return + } + data.RowsRange(func(_ int, re model1.RowEvent) bool { if ff.IsContainerForwarded(c.GetTable().Path, re.Row.ID) { re.Row.Fields[col] = "[orange::b]Ⓕ" } - } + return true + }) } -func (c *Container) decorateRows(data *render.TableData) { +func (c *Container) decorateRows(data *model1.TableData) { decorateCpuMemHeaderRows(c.App(), data) } // Name returns the component name. func (c *Container) Name() string { return containerTitle } -func (c *Container) bindDangerousKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ - ui.KeyS: ui.NewKeyAction("Shell", c.shellCmd, true), - ui.KeyA: ui.NewKeyAction("Attach", c.attachCmd, true), +func (c *Container) bindDangerousKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ + ui.KeyS: ui.NewKeyActionWithOpts( + "Shell", + c.shellCmd, + ui.ActionOpts{ + Visible: true, + Dangerous: true, + }), + ui.KeyA: ui.NewKeyActionWithOpts( + "Attach", + c.attachCmd, + ui.ActionOpts{ + Visible: true, + Dangerous: true, + }), }) } -func (c *Container) bindKeys(aa ui.KeyActions) { +func (c *Container) bindKeys(aa *ui.KeyActions) { aa.Delete(tcell.KeyCtrlSpace, ui.KeySpace) if !c.App().Config.K9s.IsReadOnly() { c.bindDangerousKeys(aa) } - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ ui.KeyF: ui.NewKeyAction("Show PortForward", c.showPFCmd, true), ui.KeyShiftF: ui.NewKeyAction("PortForward", c.portFwdCmd, true), ui.KeyShiftT: ui.NewKeyAction("Sort Restart", c.GetTable().SortColCmd("RESTARTS", false), false), }) - aa.Add(resourceSorters(c.GetTable())) + aa.Merge(resourceSorters(c.GetTable())) } func (c *Container) k9sEnv() Env { path := c.GetTable().GetSelectedItem() - row, ok := c.GetTable().GetSelectedRow(path) - if !ok { - log.Error().Msgf("unable to locate selected row for %q", path) - } - env := defaultEnv(c.App().Conn().Config(), path, c.GetTable().GetModel().Peek().Header, row) + row := c.GetTable().GetSelectedRow(path) + env := defaultEnv(c.App().Conn().Config(), path, c.GetTable().GetModel().Peek().Header(), row) env["NAMESPACE"], env["POD"] = client.Namespaced(c.GetTable().Path) return env @@ -108,7 +124,7 @@ func (c *Container) logOptions(prev bool) (*dao.LogOptions, error) { return &opts, nil } -func (c *Container) viewLogs(app *App, model ui.Tabular, gvr, path string) { +func (c *Container) viewLogs(app *App, model ui.Tabular, gvr client.GVR, path string) { c.ResourceViewer.(*LogsExtender).showLogs(c.GetTable().Path, false) } @@ -134,7 +150,10 @@ func (c *Container) showPFCmd(evt *tcell.EventKey) *tcell.EventKey { } func (c *Container) portForwardContext(ctx context.Context) context.Context { - ctx = context.WithValue(ctx, internal.KeyBenchCfg, c.App().BenchFile) + if bc := c.App().BenchFile; bc != "" { + ctx = context.WithValue(ctx, internal.KeyBenchCfg, c.App().BenchFile) + } + return context.WithValue(ctx, internal.KeyPath, c.GetTable().Path) } diff --git a/internal/view/container_test.go b/internal/view/container_test.go index 161f4cec9d..cc1133e88a 100644 --- a/internal/view/container_test.go +++ b/internal/view/container_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( diff --git a/internal/view/context.go b/internal/view/context.go index 4896d06a29..4ba51bf988 100644 --- a/internal/view/context.go +++ b/internal/view/context.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -7,6 +10,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/view/cmd" "github.com/derailed/tcell/v2" "github.com/derailed/tview" "github.com/rs/zerolog/log" @@ -33,11 +37,9 @@ func NewContext(gvr client.GVR) ResourceViewer { return &c } -func (c *Context) bindKeys(aa ui.KeyActions) { +func (c *Context) bindKeys(aa *ui.KeyActions) { aa.Delete(ui.KeyShiftA, tcell.KeyCtrlSpace, ui.KeySpace) - aa.Add(ui.KeyActions{ - ui.KeyR: ui.NewKeyAction("Rename", c.renameCmd, true), - }) + aa.Add(ui.KeyR, ui.NewKeyAction("Rename", c.renameCmd, true)) } func (c *Context) renameCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -98,7 +100,7 @@ func (c *Context) makeStyledForm() *tview.Form { return f } -func (c *Context) useCtx(app *App, model ui.Tabular, gvr, path string) { +func (c *Context) useCtx(app *App, model ui.Tabular, gvr client.GVR, path string) { log.Debug().Msgf("SWITCH CTX %q--%q", gvr, path) if err := useContext(app, path); err != nil { app.Flash().Err(err) @@ -125,5 +127,5 @@ func useContext(app *App, name string) error { return err } - return app.switchContext(name) + return app.switchContext(cmd.NewInterpreter("ctx "+name), true) } diff --git a/internal/view/context_test.go b/internal/view/context_test.go index e6ce542a74..a265459ae7 100644 --- a/internal/view/context_test.go +++ b/internal/view/context_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( diff --git a/internal/view/cow.go b/internal/view/cow.go index 140faf99bb..75b71b118e 100644 --- a/internal/view/cow.go +++ b/internal/view/cow.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -16,7 +19,7 @@ import ( type Cow struct { *tview.TextView - actions ui.KeyActions + actions *ui.KeyActions app *App says string } @@ -26,7 +29,7 @@ func NewCow(app *App, says string) *Cow { return &Cow{ TextView: tview.NewTextView(), app: app, - actions: make(ui.KeyActions), + actions: ui.NewKeyActions(), says: says, } } @@ -71,7 +74,7 @@ func cowTalk(says string, w int) string { msg := fmt.Sprintf("[red::]< [::b]Ruroh? %s[::-] >", says) buff := make([]string, 0, len(cow)+3) buff = append(buff, "[red::] "+strings.Repeat("─", len(says)+8)) - buff = append(buff, msg) + buff = append(buff, strings.TrimSuffix(msg, "\n")) buff = append(buff, " "+strings.Repeat("─", len(says)+8)) rCount := w/2 - 8 if rCount < 0 { @@ -85,13 +88,11 @@ func cowTalk(says string, w int) string { } func (c *Cow) bindKeys() { - c.actions.Set(ui.KeyActions{ - tcell.KeyEscape: ui.NewKeyAction("Back", c.resetCmd, false), - }) + c.actions.Add(tcell.KeyEscape, ui.NewKeyAction("Back", c.resetCmd, false)) } func (c *Cow) keyboard(evt *tcell.EventKey) *tcell.EventKey { - if a, ok := c.actions[ui.AsKey(evt)]; ok { + if a, ok := c.actions.Get(ui.AsKey(evt)); ok { return a.Action(evt) } @@ -110,7 +111,7 @@ func (c *Cow) resetCmd(evt *tcell.EventKey) *tcell.EventKey { } // Actions returns menu actions. -func (c *Cow) Actions() ui.KeyActions { +func (c *Cow) Actions() *ui.KeyActions { return c.actions } diff --git a/internal/view/crd.go b/internal/view/crd.go new file mode 100644 index 0000000000..7ff1a1f969 --- /dev/null +++ b/internal/view/crd.go @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package view + +import ( + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/ui" +) + +// CRD represents a crd viewer. +type CRD struct { + ResourceViewer +} + +// NewCRD returns a new viewer. +func NewCRD(gvr client.GVR) ResourceViewer { + s := CRD{ + ResourceViewer: NewBrowser(gvr), + } + s.AddBindKeysFn(s.bindKeys) + s.GetTable().SetEnterFn(s.showCRD) + + return &s +} + +func (s *CRD) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ + ui.KeyShiftV: ui.NewKeyAction("Sort Versions", s.GetTable().SortColCmd("VERSIONS", false), true), + ui.KeyShiftR: ui.NewKeyAction("Sort Group", s.GetTable().SortColCmd("GROUP", true), true), + ui.KeyShiftK: ui.NewKeyAction("Sort Kind", s.GetTable().SortColCmd("KIND", true), true), + }) +} + +func (s *CRD) showCRD(app *App, _ ui.Tabular, _ client.GVR, path string) { + _, crd := client.Namespaced(path) + app.gotoResource(crd, "", false) +} diff --git a/internal/view/cronjob.go b/internal/view/cronjob.go index cb265a3500..864a3381a8 100644 --- a/internal/view/cronjob.go +++ b/internal/view/cronjob.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -32,16 +35,16 @@ type CronJob struct { // NewCronJob returns a new viewer. func NewCronJob(gvr client.GVR) ResourceViewer { - c := CronJob{ResourceViewer: NewBrowser(gvr)} + c := CronJob{ResourceViewer: NewVulnerabilityExtender(NewBrowser(gvr))} c.AddBindKeysFn(c.bindKeys) c.GetTable().SetEnterFn(c.showJobs) return &c } -func (c *CronJob) showJobs(app *App, model ui.Tabular, gvr, path string) { +func (c *CronJob) showJobs(app *App, model ui.Tabular, gvr client.GVR, path string) { log.Debug().Msgf("Showing Jobs %q:%q -- %q", model.GetNamespace(), gvr, path) - o, err := app.factory.Get(gvr, path, true, labels.Everything()) + o, err := app.factory.Get(gvr.String(), path, true, labels.Everything()) if err != nil { app.Flash().Err(err) return @@ -68,8 +71,8 @@ func jobCtx(path, uid string) ContextFunc { } } -func (c *CronJob) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (c *CronJob) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyT: ui.NewKeyAction("Trigger", c.triggerCmd, true), ui.KeyS: ui.NewKeyAction("Suspend/Resume", c.toggleSuspendCmd, true), ui.KeyShiftL: ui.NewKeyAction("Sort LastScheduled", c.GetTable().SortColCmd(lastScheduledCol, true), false), diff --git a/internal/view/details.go b/internal/view/details.go index d65485a2dc..235b5d2bcb 100644 --- a/internal/view/details.go +++ b/internal/view/details.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -14,14 +17,18 @@ import ( "github.com/sahilm/fuzzy" ) -const detailsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] " +const ( + detailsTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] " + contentTXT = "text" + contentYAML = "yaml" +) // Details represents a generic text viewer. type Details struct { *tview.Flex text *tview.TextView - actions ui.KeyActions + actions *ui.KeyActions app *App title, subject string cmdBuff *model.FishBuff @@ -29,26 +36,31 @@ type Details struct { currentRegion, maxRegions int searchable bool fullScreen bool + contentType string } // NewDetails returns a details viewer. -func NewDetails(app *App, title, subject string, searchable bool) *Details { +func NewDetails(app *App, title, subject, contentType string, searchable bool) *Details { d := Details{ - Flex: tview.NewFlex(), - text: tview.NewTextView(), - app: app, - title: title, - subject: subject, - actions: make(ui.KeyActions), - cmdBuff: model.NewFishBuff('/', model.FilterBuffer), - model: model.NewText(), - searchable: searchable, + Flex: tview.NewFlex(), + text: tview.NewTextView(), + app: app, + title: title, + subject: subject, + actions: ui.NewKeyActions(), + cmdBuff: model.NewFishBuff('/', model.FilterBuffer), + model: model.NewText(), + searchable: searchable, + contentType: contentType, } d.AddItem(d.text, 0, 1, true) return &d } +func (d *Details) SetFilter(string) {} +func (d *Details) SetLabelFilter(map[string]string) {} + // Init initializes the viewer. func (d *Details) Init(_ context.Context) error { if d.title != "" { @@ -64,6 +76,7 @@ func (d *Details) Init(_ context.Context) error { d.app.Styles.AddListener(d) d.StylesChanged(d.app.Styles) + d.setFullScreen(d.app.Config.K9s.UI.DefaultsToFullScreen) d.app.Prompt().SetModel(d.cmdBuff) d.cmdBuff.AddListener(d) @@ -82,25 +95,23 @@ func (d *Details) InCmdMode() bool { // TextChanged notifies the model changed. func (d *Details) TextChanged(lines []string) { - d.text.SetText(colorizeYAML(d.app.Styles.Views().Yaml, strings.Join(lines, "\n"))) + switch d.contentType { + case contentYAML: + d.text.SetText(colorizeYAML(d.app.Styles.Views().Yaml, strings.Join(lines, "\n"))) + default: + d.text.SetText(strings.Join(lines, "\n")) + } d.text.ScrollToBeginning() } // TextFiltered notifies when the filter changed. func (d *Details) TextFiltered(lines []string, matches fuzzy.Matches) { - d.currentRegion, d.maxRegions = 0, 0 - - ll := make([]string, len(lines)) - copy(ll, lines) - for _, m := range matches { - loc, line := m.MatchedIndexes, ll[m.Index] - ll[m.Index] = line[:loc[0]] + fmt.Sprintf(`<<<"search_%d">>>`, d.maxRegions) + line[loc[0]:loc[1]] + `<<<"">>>` + line[loc[1]:] - d.maxRegions++ - } + d.currentRegion, d.maxRegions = 0, len(matches) + ll := linesWithRegions(lines, matches) d.text.SetText(colorizeYAML(d.app.Styles.Views().Yaml, strings.Join(ll, "\n"))) d.text.Highlight() - if d.maxRegions > 0 { + if len(matches) > 0 { d.text.Highlight("search_0") d.text.ScrollToHighlight() } @@ -121,7 +132,7 @@ func (d *Details) BufferActive(state bool, k model.BufferKind) { } func (d *Details) bindKeys() { - d.actions.Set(ui.KeyActions{ + d.actions.Bulk(ui.KeyMap{ tcell.KeyEnter: ui.NewSharedKeyAction("Filter", d.filterCmd, false), tcell.KeyEscape: ui.NewKeyAction("Back", d.resetCmd, false), tcell.KeyCtrlS: ui.NewKeyAction("Save", d.saveCmd, false), @@ -139,7 +150,7 @@ func (d *Details) bindKeys() { } func (d *Details) keyboard(evt *tcell.EventKey) *tcell.EventKey { - if a, ok := d.actions[ui.AsKey(evt)]; ok { + if a, ok := d.actions.Get(ui.AsKey(evt)); ok { return a.Action(evt) } @@ -157,6 +168,7 @@ func (d *Details) StylesChanged(s *config.Styles) { // Update updates the view content. func (d *Details) Update(buff string) *Details { d.model.SetText(buff) + return d } @@ -170,7 +182,7 @@ func (d *Details) SetSubject(s string) { } // Actions returns menu actions. -func (d *Details) Actions() ui.KeyActions { +func (d *Details) Actions() *ui.KeyActions { return d.actions } @@ -216,16 +228,20 @@ func (d *Details) toggleFullScreenCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } - d.fullScreen = !d.fullScreen - d.SetFullScreen(d.fullScreen) - d.Box.SetBorder(!d.fullScreen) - if d.fullScreen { + d.setFullScreen(!d.fullScreen) + + return nil +} + +func (d *Details) setFullScreen(isFullScreen bool) { + d.fullScreen = isFullScreen + d.SetFullScreen(isFullScreen) + d.Box.SetBorder(!isFullScreen) + if isFullScreen { d.Box.SetBorderPadding(0, 0, 0, 0) } else { d.Box.SetBorderPadding(0, 0, 1, 1) } - - return nil } func (d *Details) prevCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -287,7 +303,7 @@ func (d *Details) resetCmd(evt *tcell.EventKey) *tcell.EventKey { } func (d *Details) saveCmd(evt *tcell.EventKey) *tcell.EventKey { - if path, err := saveYAML(d.app.Config.K9s.GetScreenDumpDir(), d.app.Config.K9s.CurrentContextDir(), d.title, d.text.GetText(true)); err != nil { + if path, err := saveYAML(d.app.Config.K9s.ContextScreenDumpDir(), d.title, d.text.GetText(true)); err != nil { d.app.Flash().Err(err) } else { d.app.Flash().Infof("Log %s saved successfully!", path) diff --git a/internal/view/dir.go b/internal/view/dir.go index 38700a9345..bb34f682d9 100644 --- a/internal/view/dir.go +++ b/internal/view/dir.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -9,7 +12,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" "github.com/derailed/tcell/v2" @@ -57,22 +60,32 @@ func (d *Dir) dirContext(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeyPath, d.path) } -func (d *Dir) bindDangerousKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ - ui.KeyA: ui.NewKeyAction("Apply", d.applyCmd, true), - ui.KeyD: ui.NewKeyAction("Delete", d.delCmd, true), - ui.KeyE: ui.NewKeyAction("Edit", d.editCmd, true), +func (d *Dir) bindDangerousKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ + ui.KeyA: ui.NewKeyActionWithOpts("Apply", d.applyCmd, ui.ActionOpts{ + Visible: true, + Dangerous: true, + }), + ui.KeyD: ui.NewKeyActionWithOpts("Delete", d.delCmd, ui.ActionOpts{ + Visible: true, + Dangerous: true, + }), + ui.KeyE: ui.NewKeyActionWithOpts("Edit", d.editCmd, ui.ActionOpts{ + Visible: true, + Dangerous: true, + }), }) } -func (d *Dir) bindKeys(aa ui.KeyActions) { +func (d *Dir) bindKeys(aa *ui.KeyActions) { + // !!BOZO!! Lame! aa.Delete(ui.KeyShiftA, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) aa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL, tcell.KeyCtrlD, tcell.KeyCtrlZ) if !d.App().Config.K9s.IsReadOnly() { d.bindDangerousKeys(aa) } - aa.Add(ui.KeyActions{ - ui.KeyY: ui.NewKeyAction("YAML", d.viewCmd, true), + aa.Bulk(ui.KeyMap{ + ui.KeyY: ui.NewKeyAction(yamlAction, d.viewCmd, true), tcell.KeyEnter: ui.NewKeyAction("Goto", d.gotoCmd, true), }) } @@ -93,7 +106,7 @@ func (d *Dir) viewCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - details := NewDetails(d.App(), "YAML", sel, true).Update(string(yaml)) + details := NewDetails(d.App(), yamlAction, sel, contentYAML, true).Update(string(yaml)) if err := d.App().inject(details, false); err != nil { d.App().Flash().Err(err) } @@ -160,7 +173,7 @@ func isKustomized(sel string) bool { } kk := []string{kustomizeNoExt, kustomizeYAML, kustomizeYML} for _, f := range ff { - if config.InList(kk, f.Name()) { + if data.InList(kk, f.Name()) { return true } } @@ -213,7 +226,7 @@ func (d *Dir) applyCmd(evt *tcell.EventKey) *tcell.EventKey { res = "message:\n" + fmtResults(res) } - details := NewDetails(d.App(), "Applied Manifest", sel, true).Update(res) + details := NewDetails(d.App(), "Applied Manifest", sel, contentYAML, true).Update(res) if err := d.App().inject(details, false); err != nil { d.App().Flash().Err(err) } @@ -252,7 +265,7 @@ func (d *Dir) delCmd(evt *tcell.EventKey) *tcell.EventKey { } else { res = "message:\n" + fmtResults(res) } - details := NewDetails(d.App(), "Deleted Manifest", sel, true).Update(res) + details := NewDetails(d.App(), "Deleted Manifest", sel, contentYAML, true).Update(res) if err := d.App().inject(details, false); err != nil { d.App().Flash().Err(err) } diff --git a/internal/view/dir_int_test.go b/internal/view/dir_int_test.go index a22d715e4a..d6f4c170c0 100644 --- a/internal/view/dir_int_test.go +++ b/internal/view/dir_int_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( diff --git a/internal/view/dir_test.go b/internal/view/dir_test.go index b47f12b12c..7757eb8abf 100644 --- a/internal/view/dir_test.go +++ b/internal/view/dir_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( diff --git a/internal/view/dp.go b/internal/view/dp.go index 4c81caf94d..6db577dd07 100644 --- a/internal/view/dp.go +++ b/internal/view/dp.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -21,10 +24,12 @@ type Deploy struct { func NewDeploy(gvr client.GVR) ResourceViewer { var d Deploy d.ResourceViewer = NewPortForwardExtender( - NewRestartExtender( - NewScaleExtender( - NewImageExtender( - NewLogsExtender(NewBrowser(gvr), d.logOptions), + NewVulnerabilityExtender( + NewRestartExtender( + NewScaleExtender( + NewImageExtender( + NewLogsExtender(NewBrowser(gvr), d.logOptions), + ), ), ), ), @@ -35,8 +40,8 @@ func NewDeploy(gvr client.GVR) ResourceViewer { return &d } -func (d *Deploy) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (d *Deploy) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyShiftR: ui.NewKeyAction("Sort Ready", d.GetTable().SortColCmd(readyCol, true), false), ui.KeyShiftU: ui.NewKeyAction("Sort UpToDate", d.GetTable().SortColCmd(uptodateCol, true), false), ui.KeyShiftL: ui.NewKeyAction("Sort Available", d.GetTable().SortColCmd(availCol, true), false), @@ -48,59 +53,29 @@ func (d *Deploy) logOptions(prev bool) (*dao.LogOptions, error) { if path == "" { return nil, errors.New("you must provide a selection") } - - sts, err := d.dp(path) + dp, err := d.getInstance(path) if err != nil { return nil, err } - cc := sts.Spec.Template.Spec.Containers - var ( - co, dco string - allCos bool - ) - if c, ok := dao.GetDefaultContainer(sts.Spec.Template.ObjectMeta, sts.Spec.Template.Spec); ok { - co, dco = c, c - } else if len(cc) == 1 { - co = cc[0].Name - } else { - dco, allCos = cc[0].Name, true - } - - cfg := d.App().Config.K9s.Logger - opts := dao.LogOptions{ - Path: path, - Container: co, - Lines: int64(cfg.TailCount), - SinceSeconds: cfg.SinceSeconds, - SingleContainer: len(cc) == 1, - AllContainers: allCos, - ShowTimestamp: cfg.ShowTime, - ShowJSON: cfg.ShowJSON, - Previous: prev, - } - if co == "" { - opts.AllContainers = true - } - opts.DefaultContainer = dco - - return &opts, nil + return podLogOptions(d.App(), path, prev, dp.ObjectMeta, dp.Spec.Template.Spec), nil } -func (d *Deploy) showPods(app *App, model ui.Tabular, gvr, path string) { - var ddp dao.Deployment - dp, err := ddp.GetInstance(app.factory, path) +func (d *Deploy) showPods(app *App, model ui.Tabular, gvr client.GVR, fqn string) { + dp, err := d.getInstance(fqn) if err != nil { app.Flash().Err(err) return } - showPodsFromSelector(app, path, dp.Spec.Selector) + showPodsFromSelector(app, fqn, dp.Spec.Selector) } -func (d *Deploy) dp(path string) (*appsv1.Deployment, error) { +func (d *Deploy) getInstance(fqn string) (*appsv1.Deployment, error) { var dp dao.Deployment - return dp.GetInstance(d.App().factory, path) + dp.Init(d.App().factory, d.GVR()) + + return dp.GetInstance(fqn) } // ---------------------------------------------------------------------------- diff --git a/internal/view/dp_test.go b/internal/view/dp_test.go index 9c006652e4..aa2935c164 100644 --- a/internal/view/dp_test.go +++ b/internal/view/dp_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( @@ -13,5 +16,5 @@ func TestDeploy(t *testing.T) { assert.Nil(t, v.Init(makeCtx())) assert.Equal(t, "Deployments", v.Name()) - assert.Equal(t, 14, len(v.Hints())) + assert.Equal(t, 15, len(v.Hints())) } diff --git a/internal/view/drain_dialog.go b/internal/view/drain_dialog.go index ed56a99e2d..b576a18517 100644 --- a/internal/view/drain_dialog.go +++ b/internal/view/drain_dialog.go @@ -1,6 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( + "fmt" "strconv" "time" @@ -12,10 +16,10 @@ import ( const drainKey = "drain" // DrainFunc represents a drain callback function. -type DrainFunc func(v ResourceViewer, path string, opts dao.DrainOptions) +type DrainFunc func(v ResourceViewer, sels []string, opts dao.DrainOptions) // ShowDrain pops a node drain dialog. -func ShowDrain(view ResourceViewer, path string, opts dao.DrainOptions, okFn DrainFunc) { +func ShowDrain(view ResourceViewer, sels []string, opts dao.DrainOptions, okFn DrainFunc) { styles := view.App().Styles f := tview.NewForm() @@ -60,10 +64,17 @@ func ShowDrain(view ResourceViewer, path string, opts dao.DrainOptions, okFn Dra }) f.AddButton("OK", func() { DismissDrain(view, pages) - okFn(view, path, opts) + okFn(view, sels, opts) }) modal := tview.NewModalForm("", f) + path := "Drain " + if len(sels) == 1 { + path += sels[0] + } else { + path += fmt.Sprintf("(%d) nodes", len(sels)) + } + path += "?" modal.SetText(path) modal.SetDoneFunc(func(_ int, b string) { DismissDrain(view, pages) diff --git a/internal/view/ds.go b/internal/view/ds.go index da89b998ba..6e89f4215b 100644 --- a/internal/view/ds.go +++ b/internal/view/ds.go @@ -1,9 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( + "errors" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" + appsv1 "k8s.io/api/apps/v1" ) // DaemonSet represents a daemon set custom viewer. @@ -13,23 +19,24 @@ type DaemonSet struct { // NewDaemonSet returns a new viewer. func NewDaemonSet(gvr client.GVR) ResourceViewer { - d := DaemonSet{ - ResourceViewer: NewPortForwardExtender( + var d DaemonSet + d.ResourceViewer = NewPortForwardExtender( + NewVulnerabilityExtender( NewRestartExtender( NewImageExtender( - NewLogsExtender(NewBrowser(gvr), nil), + NewLogsExtender(NewBrowser(gvr), d.logOptions), ), ), ), - } + ) d.AddBindKeysFn(d.bindKeys) d.GetTable().SetEnterFn(d.showPods) return &d } -func (d *DaemonSet) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (d *DaemonSet) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyShiftD: ui.NewKeyAction("Sort Desired", d.GetTable().SortColCmd("DESIRED", true), false), ui.KeyShiftC: ui.NewKeyAction("Sort Current", d.GetTable().SortColCmd("CURRENT", true), false), ui.KeyShiftR: ui.NewKeyAction("Sort Ready", d.GetTable().SortColCmd(readyCol, true), false), @@ -38,7 +45,7 @@ func (d *DaemonSet) bindKeys(aa ui.KeyActions) { }) } -func (d *DaemonSet) showPods(app *App, model ui.Tabular, _, path string) { +func (d *DaemonSet) showPods(app *App, model ui.Tabular, _ client.GVR, path string) { var res dao.DaemonSet res.Init(app.factory, d.GVR()) @@ -50,3 +57,23 @@ func (d *DaemonSet) showPods(app *App, model ui.Tabular, _, path string) { showPodsFromSelector(app, path, ds.Spec.Selector) } + +func (d *DaemonSet) logOptions(prev bool) (*dao.LogOptions, error) { + path := d.GetTable().GetSelectedItem() + if path == "" { + return nil, errors.New("you must provide a selection") + } + ds, err := d.getInstance(path) + if err != nil { + return nil, err + } + + return podLogOptions(d.App(), path, prev, ds.ObjectMeta, ds.Spec.Template.Spec), nil +} + +func (d *DaemonSet) getInstance(fqn string) (*appsv1.DaemonSet, error) { + var ds dao.DaemonSet + ds.Init(d.App().factory, client.NewGVR("apps/v1/daemonsets")) + + return ds.GetInstance(fqn) +} diff --git a/internal/view/ds_test.go b/internal/view/ds_test.go index d43fe84bff..2a73445d5d 100644 --- a/internal/view/ds_test.go +++ b/internal/view/ds_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( @@ -13,5 +16,5 @@ func TestDaemonSet(t *testing.T) { assert.Nil(t, v.Init(makeCtx())) assert.Equal(t, "DaemonSets", v.Name()) - assert.Equal(t, 15, len(v.Hints())) + assert.Equal(t, 16, len(v.Hints())) } diff --git a/internal/view/env.go b/internal/view/env.go index 733beefe0f..8ac7155882 100644 --- a/internal/view/env.go +++ b/internal/view/env.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( diff --git a/internal/view/env_test.go b/internal/view/env_test.go index 2dab6cb7dd..e00744f4ea 100644 --- a/internal/view/env_test.go +++ b/internal/view/env_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( diff --git a/internal/view/event.go b/internal/view/event.go index f2bb023040..b75c975a97 100644 --- a/internal/view/event.go +++ b/internal/view/event.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -22,9 +25,9 @@ func NewEvent(gvr client.GVR) ResourceViewer { return &e } -func (e *Event) bindKeys(aa ui.KeyActions) { +func (e *Event) bindKeys(aa *ui.KeyActions) { aa.Delete(tcell.KeyCtrlD, ui.KeyE, ui.KeyA) - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ ui.KeyShiftL: ui.NewKeyAction("Sort LastSeen", e.GetTable().SortColCmd("LAST SEEN", false), false), ui.KeyShiftF: ui.NewKeyAction("Sort FirstSeen", e.GetTable().SortColCmd("FIRST SEEN", false), false), ui.KeyShiftT: ui.NewKeyAction("Sort Type", e.GetTable().SortColCmd("TYPE", true), false), diff --git a/internal/view/exec.go b/internal/view/exec.go index e80dca0a05..618dc74623 100644 --- a/internal/view/exec.go +++ b/internal/view/exec.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -13,8 +16,12 @@ import ( "syscall" "time" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/ui/dialog" "github.com/fatih/color" "github.com/rs/zerolog/log" v1 "k8s.io/api/core/v1" @@ -27,10 +34,13 @@ import ( ) const ( - shellCheck = `command -v bash >/dev/null && exec bash || exec sh` - bannerFmt = "<> Pod: %s | Container: %s \n" + shellCheck = `command -v bash >/dev/null && exec bash || exec sh` + bannerFmt = "<> Pod: %s | Container: %s \n" + outputPrefix = "[output]" ) +var editorEnvVars = []string{"KUBE_EDITOR", "K9S_EDITOR", "EDITOR"} + type shellOpts struct { clear, background bool pipes []string @@ -61,7 +71,7 @@ func runK(a *App, opts shellOpts) error { if isInsecure := a.Conn().Config().Flags().Insecure; isInsecure != nil && *isInsecure { args = append(args, "--insecure-skip-tls-verify") } - args = append(args, "--context", a.Config.K9s.CurrentContext) + args = append(args, "--context", a.Config.K9s.ActiveContextName()) if cfg := a.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" { args = append(args, "--kubeconfig", *cfg) } @@ -70,10 +80,13 @@ func runK(a *App, opts shellOpts) error { } opts.binary = bin - suspended, errChan := run(a, opts) + suspended, errChan, stChan := run(a, opts) if !suspended { return fmt.Errorf("unable to run command") } + for v := range stChan { + log.Debug().Msgf(" - %s", v) + } var errs error for e := range errChan { errs = errors.Join(errs, e) @@ -82,53 +95,65 @@ func runK(a *App, opts shellOpts) error { return errs } -func run(a *App, opts shellOpts) (bool, chan error) { +func run(a *App, opts shellOpts) (bool, chan error, chan string) { errChan := make(chan error, 1) + statusChan := make(chan string, 1) if opts.background { - if err := execute(opts); err != nil { + if err := execute(opts, statusChan); err != nil { errChan <- err a.Flash().Errf("Exec failed %q: %s", opts, err) } close(errChan) - return true, errChan + return true, errChan, statusChan } a.Halt() defer a.Resume() return a.Suspend(func() { - if err := execute(opts); err != nil { + if err := execute(opts, statusChan); err != nil { errChan <- err a.Flash().Errf("Exec failed %q: %s", opts, err) } close(errChan) - }), errChan + }), errChan, statusChan } func edit(a *App, opts shellOpts) bool { - bin, err := exec.LookPath(os.Getenv("K9S_EDITOR")) - if err != nil { - bin, err = exec.LookPath(os.Getenv("EDITOR")) - if err != nil { - log.Error().Err(err).Msgf("K9S_EDITOR|EDITOR not set") - return false + var ( + bin string + err error + ) + for _, e := range editorEnvVars { + env := os.Getenv(e) + if env == "" { + continue + } + if bin, err = exec.LookPath(env); err == nil { + break } } + if bin == "" { + a.Flash().Errf("You must set at least one of those env vars: %s", strings.Join(editorEnvVars, "|")) + return false + } opts.binary, opts.background = bin, false - suspended, errChan := run(a, opts) + suspended, errChan, _ := run(a, opts) if !suspended { a.Flash().Errf("edit command failed") } + status := true for e := range errChan { a.Flash().Err(e) - return false + status = false } - return true + + return status } -func execute(opts shellOpts) error { +func execute(opts shellOpts, statusChan chan<- string) error { if opts.clear { clearScreen() } @@ -169,7 +194,7 @@ func execute(opts shellOpts) error { } var o, e bytes.Buffer - err := pipe(ctx, opts, &o, &e, cmds...) + err := pipe(ctx, opts, statusChan, &o, &e, cmds...) if err != nil { log.Err(err).Msgf("Command failed") return errors.Join(err, fmt.Errorf("%s", e.String())) @@ -195,7 +220,7 @@ func runKu(a *App, opts shellOpts) (string, error) { if g, err := a.Conn().Config().ImpersonateGroups(); err == nil { args = append(args, "--as-group", g) } - args = append(args, "--context", a.Config.K9s.CurrentContext) + args = append(args, "--context", a.Config.K9s.ActiveContextName()) if cfg := a.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" { args = append(args, "--kubeconfig", *cfg) } @@ -230,35 +255,54 @@ func clearScreen() { const ( k9sShell = "k9s-shell" - k9sShellRetryCount = 10 - k9sShellRetryDelay = 10 * time.Second + k9sShellRetryCount = 50 + k9sShellRetryDelay = 2 * time.Second ) -func ssh(a *App, node string) error { +func launchNodeShell(v model.Igniter, a *App, node string) { if err := nukeK9sShell(a); err != nil { - return err + a.Flash().Errf("Cleaning node shell failed: %s", err) + return } + + msg := fmt.Sprintf("Launching node shell on %s...", node) + dialog.ShowPrompt(a.Styles.Dialog(), a.Content.Pages, "Launching", msg, func(ctx context.Context) { + err := launchShellPod(ctx, a, node) + if err != nil { + if !errors.Is(err, context.Canceled) { + a.Flash().Errf("Launching node shell failed: %s", err) + } + return + } + + go launchPodShell(v, a) + }, func() { + if err := nukeK9sShell(a); err != nil { + a.Flash().Errf("Cleaning node shell failed: %s", err) + return + } + }) +} + +func launchPodShell(v model.Igniter, a *App) { defer func() { if err := nukeK9sShell(a); err != nil { - log.Error().Err(err).Msgf("nuking k9s shell pod") + a.Flash().Errf("Launching node shell failed: %s", err) + return } }() - if err := launchShellPod(a, node); err != nil { - return err - } - cl := a.Config.K9s.ActiveCluster() - if cl == nil { - return fmt.Errorf("no active cluster detected") - } - ns := cl.ShellPod.Namespace + v.Stop() + defer v.Start() - return sshIn(a, client.FQN(ns, k9sShellPodName()), k9sShell) + ns := a.Config.K9s.ShellPod.Namespace + if err := sshIn(a, client.FQN(ns, k9sShellPodName()), k9sShell); err != nil { + a.Flash().Errf("Launching node shell failed: %s", err) + } } func sshIn(a *App, fqn, co string) error { - cl := a.Config.K9s.ActiveCluster() - cfg := cl.ShellPod + cfg := a.Config.K9s.ShellPod os, err := getPodOS(a.factory, fqn) if err != nil { return fmt.Errorf("os detect failed: %w", err) @@ -287,13 +331,15 @@ func sshIn(a *App, fqn, co string) error { } func nukeK9sShell(a *App) error { - clName := a.Config.K9s.CurrentCluster - if !a.Config.K9s.Clusters[clName].FeatureGates.NodeShell { + ct, err := a.Config.K9s.ActiveContext() + if err != nil { + return err + } + if !ct.FeatureGates.NodeShell { return nil } - cl := a.Config.K9s.ActiveCluster() - ns := cl.ShellPod.Namespace + ns := a.Config.K9s.ShellPod.Namespace ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() @@ -310,29 +356,33 @@ func nukeK9sShell(a *App) error { return err } -func launchShellPod(a *App, node string) error { - a.Flash().Infof("Launching node shell on %s...", node) - cl := a.Config.K9s.ActiveCluster() - ns := cl.ShellPod.Namespace - spec := k9sShellPod(node, cl.ShellPod) - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() +func launchShellPod(ctx context.Context, a *App, node string) error { + var ( + spo = a.Config.K9s.ShellPod + spec = k9sShellPod(node, spo) + ) dial, err := a.Conn().Dial() if err != nil { return err } - conn := dial.CoreV1().Pods(ns) - if _, err := conn.Create(ctx, spec, metav1.CreateOptions{}); err != nil { + + conn := dial.CoreV1().Pods(spo.Namespace) + if _, err = conn.Create(ctx, spec, metav1.CreateOptions{}); err != nil { return err } for i := 0; i < k9sShellRetryCount; i++ { - o, err := a.factory.Get("v1/pods", client.FQN(ns, k9sShellPodName()), true, labels.Everything()) + o, err := a.factory.Get("v1/pods", client.FQN(spo.Namespace, k9sShellPodName()), true, labels.Everything()) if err != nil { - time.Sleep(k9sShellRetryDelay) - continue + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(k9sShellRetryDelay): + continue + } } + var pod v1.Pod if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod); err != nil { return err @@ -341,7 +391,12 @@ func launchShellPod(a *App, node string) error { if pod.Status.Phase == v1.PodRunning { return nil } - time.Sleep(k9sShellRetryDelay) + + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(k9sShellRetryDelay): + } } return fmt.Errorf("unable to launch shell pod on node %s", node) @@ -351,14 +406,15 @@ func k9sShellPodName() string { return fmt.Sprintf("%s-%d", k9sShell, os.Getpid()) } -func k9sShellPod(node string, cfg *config.ShellPod) *v1.Pod { +func k9sShellPod(node string, cfg config.ShellPod) *v1.Pod { var grace int64 var priv bool = true log.Debug().Msgf("Shell Config %#v", cfg) c := v1.Container{ - Name: k9sShell, - Image: cfg.Image, + Name: k9sShell, + Image: cfg.Image, + ImagePullPolicy: cfg.ImagePullPolicy, VolumeMounts: []v1.VolumeMount{ { Name: "root-vol", @@ -368,6 +424,7 @@ func k9sShellPod(node string, cfg *config.ShellPod) *v1.Pod { }, Resources: asResource(cfg.Limits), Stdin: true, + TTY: cfg.TTY, SecurityContext: &v1.SecurityContext{ Privileged: &priv, }, @@ -390,6 +447,7 @@ func k9sShellPod(node string, cfg *config.ShellPod) *v1.Pod { RestartPolicy: v1.RestartPolicyNever, HostPID: true, HostNetwork: true, + ImagePullSecrets: cfg.ImagePullSecrets, TerminationGracePeriodSeconds: &grace, Volumes: []v1.Volume{ { @@ -420,7 +478,7 @@ func asResource(r config.Limits) v1.ResourceRequirements { } } -func pipe(_ context.Context, opts shellOpts, w, e io.Writer, cmds ...*exec.Cmd) error { +func pipe(_ context.Context, opts shellOpts, statusChan chan<- string, w, e *bytes.Buffer, cmds ...*exec.Cmd) error { if len(cmds) == 0 { return nil } @@ -428,15 +486,33 @@ func pipe(_ context.Context, opts shellOpts, w, e io.Writer, cmds ...*exec.Cmd) if len(cmds) == 1 { cmd := cmds[0] if opts.background { - cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, w, e - return cmd.Run() + go func() { + cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, w, e + if err := cmd.Run(); err != nil { + log.Error().Err(err).Msgf("Command failed: %s", err) + } else { + for _, l := range strings.Split(w.String(), "\n") { + if l != "" { + statusChan <- fmt.Sprintf("%s %s", outputPrefix, l) + } + } + statusChan <- fmt.Sprintf("Command completed successfully: %q", render.Truncate(cmd.String(), 20)) + log.Info().Msgf("Command completed successfully: %q", cmd.String()) + } + close(statusChan) + }() + return nil } cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr _, _ = cmd.Stdout.Write([]byte(opts.banner)) log.Debug().Msgf("Running Start") err := cmd.Run() - log.Debug().Msgf("Running Done: %s", err) + log.Debug().Msgf("Running Done: %v", err) + if err == nil { + statusChan <- fmt.Sprintf("Command completed successfully: %q", cmd.String()) + } + close(statusChan) return err } diff --git a/internal/view/group.go b/internal/view/group.go index ad074a002d..0cfe42ddb5 100644 --- a/internal/view/group.go +++ b/internal/view/group.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -23,9 +26,9 @@ func NewGroup(gvr client.GVR) ResourceViewer { return &g } -func (g *Group) bindKeys(aa ui.KeyActions) { +func (g *Group) bindKeys(aa *ui.KeyActions) { aa.Delete(ui.KeyShiftA, ui.KeyShiftP, tcell.KeyCtrlSpace, ui.KeySpace) - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ tcell.KeyEnter: ui.NewKeyAction("Rules", g.policyCmd, true), ui.KeyShiftK: ui.NewKeyAction("Sort Kind", g.GetTable().SortColCmd("KIND", true), false), }) diff --git a/internal/view/helm.go b/internal/view/helm.go deleted file mode 100644 index 6e6e296880..0000000000 --- a/internal/view/helm.go +++ /dev/null @@ -1,78 +0,0 @@ -package view - -import ( - "context" - - "github.com/derailed/k9s/internal" - "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/model" - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/tcell/v2" - "github.com/rs/zerolog/log" -) - -// Helm represents a helm chart view. -type Helm struct { - ResourceViewer - - Values *model.Values -} - -// NewHelm returns a new alias view. -func NewHelm(gvr client.GVR) ResourceViewer { - c := Helm{ - ResourceViewer: NewBrowser(gvr), - } - c.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen) - c.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorMediumSpringGreen).Attributes(tcell.AttrNone)) - c.AddBindKeysFn(c.bindKeys) - c.SetContextFn(c.chartContext) - - return &c -} - -func (c *Helm) chartContext(ctx context.Context) context.Context { - return ctx -} - -func (c *Helm) bindKeys(aa ui.KeyActions) { - aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) - aa.Add(ui.KeyActions{ - ui.KeyShiftN: ui.NewKeyAction("Sort Name", c.GetTable().SortColCmd(nameCol, true), false), - ui.KeyShiftS: ui.NewKeyAction("Sort Status", c.GetTable().SortColCmd(statusCol, true), false), - ui.KeyShiftA: ui.NewKeyAction("Sort Age", c.GetTable().SortColCmd(ageCol, true), false), - ui.KeyV: ui.NewKeyAction("Values", c.getValsCmd(), true), - }) -} - -func (c *Helm) getValsCmd() func(evt *tcell.EventKey) *tcell.EventKey { - return func(evt *tcell.EventKey) *tcell.EventKey { - path := c.GetTable().GetSelectedItem() - if path == "" { - return evt - } - c.Values = model.NewValues(c.GVR(), path) - v := NewLiveView(c.App(), "Values", c.Values) - v.actions.Add(ui.KeyActions{ - ui.KeyV: ui.NewKeyAction("Toggle All Values", c.toggleValuesCmd, true), - }) - if err := v.app.inject(v, false); err != nil { - v.app.Flash().Err(err) - } - return nil - } -} - -func (c *Helm) toggleValuesCmd(evt *tcell.EventKey) *tcell.EventKey { - c.Values.ToggleValues() - if err := c.Values.Refresh(c.defaultCtx()); err != nil { - log.Error().Err(err).Msgf("helm refresh failed") - return nil - } - c.App().Flash().Infof("Values toggled") - return nil -} - -func (c *Helm) defaultCtx() context.Context { - return context.WithValue(context.Background(), internal.KeyFactory, c.App().factory) -} diff --git a/internal/view/helm_chart.go b/internal/view/helm_chart.go new file mode 100644 index 0000000000..c3d595baf5 --- /dev/null +++ b/internal/view/helm_chart.go @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package view + +import ( + "context" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tcell/v2" +) + +// HelmChart represents a helm chart view. +type HelmChart struct { + ResourceViewer +} + +// NewHelmChart returns a new helm-chart view. +func NewHelmChart(gvr client.GVR) ResourceViewer { + c := HelmChart{ + ResourceViewer: NewValueExtender(NewBrowser(gvr)), + } + c.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen) + c.GetTable().SetSelectedStyle(tcell.StyleDefault. + Foreground(tcell.ColorWhite). + Background(tcell.ColorMediumSpringGreen).Attributes(tcell.AttrNone)) + c.AddBindKeysFn(c.bindKeys) + c.GetTable().SetEnterFn(c.viewReleases) + c.SetContextFn(c.chartContext) + + return &c +} + +func (c *HelmChart) chartContext(ctx context.Context) context.Context { + return ctx +} + +func (c *HelmChart) bindKeys(aa *ui.KeyActions) { + aa.Delete(tcell.KeyCtrlS) + aa.Bulk(ui.KeyMap{ + ui.KeyR: ui.NewKeyAction("Releases", c.historyCmd, true), + ui.KeyShiftS: ui.NewKeyAction("Sort Status", c.GetTable().SortColCmd(statusCol, true), false), + }) +} + +func (c *HelmChart) viewReleases(app *App, model ui.Tabular, _ client.GVR, path string) { + v := NewHistory(client.NewGVR("helm-history")) + v.SetContextFn(c.helmContext) + if err := app.inject(v, false); err != nil { + app.Flash().Err(err) + } +} + +func (c *HelmChart) historyCmd(evt *tcell.EventKey) *tcell.EventKey { + path := c.GetTable().GetSelectedItem() + if path == "" { + return evt + } + c.viewReleases(c.App(), c.GetTable().GetModel(), c.GVR(), path) + + return nil +} + +func (c *HelmChart) helmContext(ctx context.Context) context.Context { + path := c.GetTable().GetSelectedItem() + if path == "" { + return ctx + } + ctx = context.WithValue(ctx, internal.KeyFQN, path) + + return context.WithValue(ctx, internal.KeyPath, path) +} diff --git a/internal/view/helm_history.go b/internal/view/helm_history.go new file mode 100644 index 0000000000..a2b5e5a922 --- /dev/null +++ b/internal/view/helm_history.go @@ -0,0 +1,131 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package view + +import ( + "context" + "fmt" + "strings" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render/helm" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/ui/dialog" + "github.com/derailed/tcell/v2" +) + +// History represents a helm History view. +type History struct { + ResourceViewer + + Values *model.RevValues +} + +// NewHistory returns a new helm-history view. +func NewHistory(gvr client.GVR) ResourceViewer { + h := History{ + ResourceViewer: NewValueExtender(NewBrowser(gvr)), + } + h.GetTable().SetColorerFn(helm.History{}.ColorerFunc()) + h.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen) + h.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorMediumSpringGreen).Attributes(tcell.AttrNone)) + h.AddBindKeysFn(h.bindKeys) + h.SetContextFn(h.HistoryContext) + h.GetTable().SetEnterFn(h.getValsCmd) + + return &h +} + +// Init initializes the view +func (h *History) Init(ctx context.Context) error { + if err := h.ResourceViewer.Init(ctx); err != nil { + return err + } + h.GetTable().SetSortCol("REVISION", false) + + return nil +} + +func (h *History) HistoryContext(ctx context.Context) context.Context { + return ctx +} + +func (h *History) bindKeys(aa *ui.KeyActions) { + if !h.App().Config.K9s.IsReadOnly() { + h.bindDangerousKeys(aa) + } + + aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace, tcell.KeyCtrlD) + aa.Bulk(ui.KeyMap{ + ui.KeyShiftN: ui.NewKeyAction("Sort Revision", h.GetTable().SortColCmd("REVISION", true), false), + ui.KeyShiftS: ui.NewKeyAction("Sort Status", h.GetTable().SortColCmd("STATUS", true), false), + ui.KeyShiftA: ui.NewKeyAction("Sort Age", h.GetTable().SortColCmd("AGE", true), false), + }) +} + +func (h *History) getValsCmd(app *App, _ ui.Tabular, _ client.GVR, path string) { + ns, n := client.Namespaced(path) + tt := strings.Split(n, ":") + if len(tt) < 2 { + app.Flash().Err(fmt.Errorf("unable to parse version in %q", path)) + return + } + name, rev := tt[0], tt[1] + h.Values = model.NewRevValues(h.GVR(), client.FQN(ns, name), rev) + v := NewLiveView(h.App(), "Values", h.Values) + if err := v.app.inject(v, false); err != nil { + v.app.Flash().Err(err) + } +} + +func (h *History) bindDangerousKeys(aa *ui.KeyActions) { + aa.Add(ui.KeyR, ui.NewKeyActionWithOpts("RollBackTo...", h.rollbackCmd, + ui.ActionOpts{ + Visible: true, + Dangerous: true, + }, + )) +} + +func (h *History) rollbackCmd(evt *tcell.EventKey) *tcell.EventKey { + path := h.GetTable().GetSelectedItem() + if path == "" { + return evt + } + + ns, nrev := client.Namespaced(path) + tt := strings.Split(nrev, ":") + n, rev := nrev, "" + if len(tt) == 2 { + n, rev = tt[0], tt[1] + } + + h.Stop() + defer h.Start() + msg := fmt.Sprintf("RollingBack chart [yellow::b]%s[-::-] to release <[orangered::b]%s[-::-]>?", n, rev) + dialog.ShowConfirmAck(h.App().App, h.App().Content.Pages, n, false, "Confirm Rollback", msg, func() { + ctx, cancel := context.WithTimeout(context.Background(), h.App().Conn().Config().CallTimeout()) + defer cancel() + if err := h.rollback(ctx, client.FQN(ns, n), rev); err != nil { + h.App().Flash().Err(err) + } else { + h.App().Flash().Infof("Rollout restart in progress for char `%s...", n) + } + }, func() {}) + + return nil +} + +func (h *History) rollback(ctx context.Context, path, rev string) error { + var hm dao.HelmHistory + hm.Init(h.App().factory, h.GVR()) + if err := hm.Rollback(ctx, path, rev); err != nil { + return err + } + h.Refresh() + + return nil +} diff --git a/internal/view/help.go b/internal/view/help.go index fb95384c5e..4347a43cfa 100644 --- a/internal/view/help.go +++ b/internal/view/help.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -40,6 +43,9 @@ func NewHelp(app *App) *Help { } } +func (h *Help) SetFilter(string) {} +func (h *Help) SetLabelFilter(map[string]string) {} + // Init initializes the component. func (h *Help) Init(ctx context.Context) error { if err := h.Table.Init(ctx); err != nil { @@ -71,7 +77,7 @@ func (h *Help) StylesChanged(s *config.Styles) { func (h *Help) bindKeys() { h.Actions().Delete(ui.KeySpace, tcell.KeyCtrlSpace, tcell.KeyCtrlS, ui.KeySlash) - h.Actions().Set(ui.KeyActions{ + h.Actions().Bulk(ui.KeyMap{ tcell.KeyEscape: ui.NewKeyAction("Back", h.app.PrevCmd, true), ui.KeyHelp: ui.NewKeyAction("Back", h.app.PrevCmd, false), tcell.KeyEnter: ui.NewKeyAction("Back", h.app.PrevCmd, false), @@ -185,7 +191,7 @@ func (h *Help) showNav() model.MenuHints { func (h *Help) showHotKeys() (model.MenuHints, error) { hh := config.NewHotKeys() - if err := hh.Load(); err != nil { + if err := hh.Load(h.App().Config.ContextHotkeysPath()); err != nil { return nil, fmt.Errorf("no hotkey configuration found") } kk := make(sort.StringSlice, 0, len(hh.HotKey)) diff --git a/internal/view/help_test.go b/internal/view/help_test.go index 2f29a6b765..b6f19c7831 100644 --- a/internal/view/help_test.go +++ b/internal/view/help_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( @@ -21,8 +24,8 @@ func TestHelp(t *testing.T) { v := view.NewHelp(app) assert.Nil(t, v.Init(ctx)) - assert.Equal(t, 28, v.GetRowCount()) - assert.Equal(t, 6, v.GetColumnCount()) + assert.Equal(t, 29, v.GetRowCount()) + assert.Equal(t, 8, v.GetColumnCount()) assert.Equal(t, "", strings.TrimSpace(v.GetCell(1, 0).Text)) assert.Equal(t, "Attach", strings.TrimSpace(v.GetCell(1, 1).Text)) } diff --git a/internal/view/helpers.go b/internal/view/helpers.go index da65c794e8..8ec027a9b0 100644 --- a/internal/view/helpers.go +++ b/internal/view/helpers.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -11,21 +14,48 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/view/cmd" "github.com/derailed/tcell/v2" "github.com/derailed/tview" "github.com/rs/zerolog/log" + "github.com/sahilm/fuzzy" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +func aliasesFor(m v1.APIResource, aa []string) map[string]struct{} { + rr := make(map[string]struct{}) + rr[m.Name] = struct{}{} + for _, a := range aa { + rr[a] = struct{}{} + } + if m.ShortNames != nil { + for _, a := range m.ShortNames { + rr[a] = struct{}{} + } + } + if m.SingularName != "" { + rr[m.SingularName] = struct{}{} + } + + return rr +} + func clipboardWrite(text string) error { return clipboard.WriteAll(text) } +func sanitizeEsc(s string) string { + return strings.ReplaceAll(s, "[]", "]") +} + func cpCmd(flash *model.Flash, v *tview.TextView) func(*tcell.EventKey) *tcell.EventKey { return func(evt *tcell.EventKey) *tcell.EventKey { - if err := clipboardWrite(v.GetText(true)); err != nil { + if err := clipboardWrite(sanitizeEsc(v.GetText(true))); err != nil { flash.Err(err) return evt } @@ -77,42 +107,42 @@ func k8sEnv(c *client.Config) Env { } } -func defaultEnv(c *client.Config, path string, header render.Header, row render.Row) Env { +func defaultEnv(c *client.Config, path string, header model1.Header, row *model1.Row) Env { env := k8sEnv(c) env["NAMESPACE"], env["NAME"] = client.Namespaced(path) - for _, col := range header.Columns(true) { - i := header.IndexOf(col, true) - if i >= 0 && i < len(row.Fields) { - env["COL-"+col] = row.Fields[i] + if row == nil { + return env + } + for _, col := range header.ColumnNames(true) { + idx, ok := header.IndexOf(col, true) + if ok && idx < len(row.Fields) { + env["COL-"+col] = row.Fields[idx] } } return env } -func describeResource(app *App, m ui.Tabular, gvr, path string) { - v := NewLiveView(app, "Describe", model.NewDescribe(client.NewGVR(gvr), path)) +func describeResource(app *App, m ui.Tabular, gvr client.GVR, path string) { + v := NewLiveView(app, "Describe", model.NewDescribe(gvr, path)) if err := app.inject(v, false); err != nil { app.Flash().Err(err) } } -func showPodsWithLabels(app *App, path string, sel map[string]string) { - labels := make([]string, 0, len(sel)) - for k, v := range sel { - labels = append(labels, fmt.Sprintf("%s=%s", k, v)) +func toLabelsStr(labels map[string]string) string { + ll := make([]string, 0, len(labels)) + for k, v := range labels { + ll = append(ll, fmt.Sprintf("%s=%s", k, v)) } - showPods(app, path, strings.Join(labels, ","), "") + + return strings.Join(ll, ",") } func showPods(app *App, path, labelSel, fieldSel string) { - if err := app.switchNS(client.AllNamespaces); err != nil { - app.Flash().Err(err) - return - } - v := NewPod(client.NewGVR("v1/pods")) - v.SetContextFn(podCtx(app, path, labelSel, fieldSel)) + v.SetContextFn(podCtx(app, path, fieldSel)) + v.SetLabelFilter(cmd.ToLabels(labelSel)) ns, _ := client.Namespaced(path) if err := app.Config.SetActiveNamespace(ns); err != nil { @@ -123,19 +153,9 @@ func showPods(app *App, path, labelSel, fieldSel string) { } } -func podCtx(app *App, path, labelSel, fieldSel string) ContextFunc { +func podCtx(app *App, path, fieldSel string) ContextFunc { return func(ctx context.Context) context.Context { ctx = context.WithValue(ctx, internal.KeyPath, path) - ctx = context.WithValue(ctx, internal.KeyLabels, labelSel) - - ns, _ := client.Namespaced(path) - mx := client.NewMetricsServer(app.factory.Client()) - nmx, err := mx.FetchPodsMetrics(ctx, ns) - if err != nil { - log.Debug().Err(err).Msgf("No pods metrics") - } - ctx = context.WithValue(ctx, internal.KeyMetrics, nmx) - return context.WithValue(ctx, internal.KeyFields, fieldSel) } } @@ -157,7 +177,7 @@ func asKey(key string) (tcell.Key, error) { } } - return 0, fmt.Errorf("no matching key found %s", key) + return 0, fmt.Errorf("invalid key specified: %q", key) } // FwFQN returns a fully qualified ns/name:container id. @@ -199,8 +219,8 @@ func fqn(ns, n string) string { return ns + "/" + n } -func decorateCpuMemHeaderRows(app *App, data *render.TableData) { - for colIndex, header := range data.Header { +func decorateCpuMemHeaderRows(app *App, data *model1.TableData) { + for colIndex, header := range data.Header() { var check string if header.Name == "%CPU/L" { check = "cpu" @@ -211,25 +231,50 @@ func decorateCpuMemHeaderRows(app *App, data *render.TableData) { if len(check) == 0 { continue } - for _, re := range data.RowEvents { + data.RowsRange(func(_ int, re model1.RowEvent) bool { if re.Row.Fields[colIndex] == render.NAValue { - continue + return true } n, err := strconv.Atoi(re.Row.Fields[colIndex]) if err != nil { - continue + return true } if n > 100 { n = 100 } severity := app.Config.K9s.Thresholds.LevelFor(check, n) if severity == config.SeverityLow { - continue + return true } color := app.Config.K9s.Thresholds.SeverityColor(check, n) if len(color) > 0 { re.Row.Fields[colIndex] = "[" + color + "::b]" + re.Row.Fields[colIndex] } + + return true + }) + } +} + +func matchTag(i int, s string) string { + return `<<<"search_` + strconv.Itoa(i) + `">>>` + s + `<<<"">>>` +} + +func linesWithRegions(lines []string, matches fuzzy.Matches) []string { + ll := make([]string, len(lines)) + copy(ll, lines) + offsetForLine := make(map[int]int) + for i, m := range matches { + for _, loc := range dao.ContinuousRanges(m.MatchedIndexes) { + start, end := loc[0]+offsetForLine[m.Index], loc[1]+offsetForLine[m.Index] + line := ll[m.Index] + if end > len(line) { + end = len(line) + } + regionStr := matchTag(i, line[start:end]) + ll[m.Index] = line[:start] + regionStr + line[end:] + offsetForLine[m.Index] += len(regionStr) - (end - start) } } + return ll } diff --git a/internal/view/helpers_test.go b/internal/view/helpers_test.go index a2c182e202..5c2ddbe217 100644 --- a/internal/view/helpers_test.go +++ b/internal/view/helpers_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -8,9 +11,12 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/derailed/tcell/v2" "github.com/rs/zerolog" + "github.com/sahilm/fuzzy" "github.com/stretchr/testify/assert" "k8s.io/cli-runtime/pkg/genericclioptions" ) @@ -55,7 +61,7 @@ func TestParsePFAnn(t *testing.T) { } func TestExtractApp(t *testing.T) { - app := NewApp(config.NewConfig(nil)) + app := NewApp(mock.NewMockConfig()) uu := map[string]struct { app *App @@ -103,7 +109,7 @@ func TestAsKey(t *testing.T) { e tcell.Key }{ "cool": {k: "Ctrl-A", e: tcell.KeyCtrlA}, - "miss": {k: "fred", e: 0, err: errors.New("no matching key found fred")}, + "miss": {k: "fred", e: 0, err: errors.New(`invalid key specified: "fred"`)}, } for k := range uu { @@ -144,15 +150,15 @@ func TestK9sEnv(t *testing.T) { KubeConfig: &cfg, } c := client.NewConfig(&flags) - h := render.Header{ + h := model1.Header{ {Name: "A"}, {Name: "B"}, {Name: "C"}, } - r := render.Row{ + r := model1.Row{ Fields: []string{"a1", "b1", "c1"}, } - env := defaultEnv(c, "fred/blee", h, r) + env := defaultEnv(c, "fred/blee", h, &r) assert.Equal(t, 10, len(env)) assert.Equal(t, cl, env["CLUSTER"]) @@ -263,3 +269,61 @@ func TestContainerID(t *testing.T) { }) } } + +func Test_linesWithRegions(t *testing.T) { + uu := map[string]struct { + lines []string + matches fuzzy.Matches + e []string + }{ + "empty-lines": { + e: []string{}, + }, + "no-match": { + lines: []string{"bar"}, + e: []string{"bar"}, + }, + "single-match": { + lines: []string{"foo", "bar", "baz"}, + matches: fuzzy.Matches{ + {Index: 1, MatchedIndexes: []int{0, 1, 2}}, + }, + e: []string{"foo", matchTag(0, "bar"), "baz"}, + }, + "single-character": { + lines: []string{"foo", "bar", "baz"}, + matches: fuzzy.Matches{ + {Index: 1, MatchedIndexes: []int{1}}, + }, + e: []string{"foo", "b" + matchTag(0, "a") + "r", "baz"}, + }, + "multiple-matches": { + lines: []string{"foo", "bar", "baz"}, + matches: fuzzy.Matches{ + {Index: 1, MatchedIndexes: []int{0, 1, 2}}, + {Index: 2, MatchedIndexes: []int{0, 1, 2}}, + }, + e: []string{"foo", matchTag(0, "bar"), matchTag(1, "baz")}, + }, + "multiple-matches-same-line": { + lines: []string{"foosfoo baz", "dfbarfoos bar"}, + matches: fuzzy.Matches{ + {Index: 0, MatchedIndexes: []int{0, 1, 2}}, + {Index: 0, MatchedIndexes: []int{4, 5, 6}}, + {Index: 1, MatchedIndexes: []int{5, 6, 7}}, + }, + e: []string{ + matchTag(0, "foo") + "s" + matchTag(1, "foo") + " baz", + "dfbar" + matchTag(2, "foo") + "s bar", + }, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + t.Parallel() + assert.Equal(t, u.e, linesWithRegions(u.lines, u.matches)) + }) + } +} diff --git a/internal/view/image_extender.go b/internal/view/image_extender.go index 64f953a948..bc8f2a7b7c 100644 --- a/internal/view/image_extender.go +++ b/internal/view/image_extender.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -53,13 +56,11 @@ func NewImageExtender(r ResourceViewer) ResourceViewer { return &s } -func (s *ImageExtender) bindKeys(aa ui.KeyActions) { +func (s *ImageExtender) bindKeys(aa *ui.KeyActions) { if s.App().Config.K9s.IsReadOnly() { return } - aa.Add(ui.KeyActions{ - ui.KeyI: ui.NewKeyAction("Set Image", s.setImageCmd, false), - }) + aa.Add(ui.KeyI, ui.NewKeyAction("Set Image", s.setImageCmd, false)) } func (s *ImageExtender) setImageCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/view/img_scan.go b/internal/view/img_scan.go new file mode 100644 index 0000000000..58b1002d80 --- /dev/null +++ b/internal/view/img_scan.go @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package view + +import ( + "errors" + "runtime" + "strings" + + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tcell/v2" +) + +const ( + imgScanTitle = "Scans" + browseOSX = "open" + browseLinux = "sensible-browser" + cveGovURL = "https://nvd.nist.gov/vuln/detail/" + ghsaURL = "https://github.com/advisories/" +) + +// ImageScan represents an image vulnerability scan view. +type ImageScan struct { + ResourceViewer +} + +// NewImageScan returns a new scans view. +func NewImageScan(gvr client.GVR) ResourceViewer { + v := ImageScan{} + v.ResourceViewer = NewBrowser(gvr) + v.AddBindKeysFn(v.bindKeys) + v.GetTable().SetEnterFn(v.viewCVE) + v.GetTable().SetSortCol("SEVERITY", true) + + return &v +} + +// Name returns the component name. +func (s *ImageScan) Name() string { return imgScanTitle } + +func (c *ImageScan) bindKeys(aa *ui.KeyActions) { + aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlZ, tcell.KeyCtrlW) + + aa.Bulk(ui.KeyMap{ + ui.KeyShiftL: ui.NewKeyAction("Sort Lib", c.GetTable().SortColCmd("LIBRARY", false), true), + ui.KeyShiftS: ui.NewKeyAction("Sort Severity", c.GetTable().SortColCmd("SEVERITY", false), true), + ui.KeyShiftF: ui.NewKeyAction("Sort Fixed-in", c.GetTable().SortColCmd("FIXED-IN", false), true), + ui.KeyShiftV: ui.NewKeyAction("Sort Vulnerability", c.GetTable().SortColCmd("VULNERABILITY", false), true), + }) +} + +func (s *ImageScan) viewCVE(app *App, _ ui.Tabular, _ client.GVR, path string) { + bin := browseLinux + if runtime.GOOS == "darwin" { + bin = browseOSX + } + + tt := strings.Split(path, "|") + if len(tt) < 7 { + app.Flash().Errf("parse path failed: %s", path) + } + cve := tt[render.CVEParseIdx] + site := cveGovURL + if strings.Index(cve, "GHSA") == 0 { + site = ghsaURL + } + site += cve + + ok, errChan, _ := run(app, shellOpts{ + background: true, + binary: bin, + args: []string{site}, + }) + if !ok { + app.Flash().Errf("unable to run browser command") + return + } + var errs error + for e := range errChan { + errs = errors.Join(e) + } + if errs != nil { + app.Flash().Err(errs) + } +} diff --git a/internal/view/job.go b/internal/view/job.go index e9421d49dc..c456227392 100644 --- a/internal/view/job.go +++ b/internal/view/job.go @@ -1,7 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( + "errors" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" batchv1 "k8s.io/api/batch/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -16,15 +22,21 @@ type Job struct { // NewJob returns a new viewer. func NewJob(gvr client.GVR) ResourceViewer { - j := Job{ResourceViewer: NewLogsExtender(NewBrowser(gvr), nil)} + var j Job + + j.ResourceViewer = NewVulnerabilityExtender( + NewOwnerExtender( + NewLogsExtender(NewBrowser(gvr), j.logOptions), + ), + ) j.GetTable().SetEnterFn(j.showPods) j.GetTable().SetSortCol("AGE", true) return &j } -func (*Job) showPods(app *App, model ui.Tabular, gvr, path string) { - o, err := app.factory.Get(gvr, path, true, labels.Everything()) +func (*Job) showPods(app *App, model ui.Tabular, gvr client.GVR, path string) { + o, err := app.factory.Get(gvr.String(), path, true, labels.Everything()) if err != nil { app.Flash().Err(err) return @@ -39,3 +51,23 @@ func (*Job) showPods(app *App, model ui.Tabular, gvr, path string) { showPodsFromSelector(app, path, job.Spec.Selector) } + +func (j *Job) logOptions(prev bool) (*dao.LogOptions, error) { + path := j.GetTable().GetSelectedItem() + if path == "" { + return nil, errors.New("you must provide a selection") + } + job, err := j.getInstance(path) + if err != nil { + return nil, err + } + + return podLogOptions(j.App(), path, prev, job.ObjectMeta, job.Spec.Template.Spec), nil +} + +func (j *Job) getInstance(fqn string) (*batchv1.Job, error) { + var job dao.Job + job.Init(j.App().factory, client.NewGVR("batch/v1/jobs")) + + return job.GetInstance(fqn) +} diff --git a/internal/view/live_view.go b/internal/view/live_view.go index d9d6af892b..a928f2e29c 100644 --- a/internal/view/live_view.go +++ b/internal/view/live_view.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -5,7 +8,6 @@ import ( "fmt" "strconv" "strings" - "time" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/config" @@ -17,7 +19,10 @@ import ( "github.com/sahilm/fuzzy" ) -const liveViewTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] " +const ( + liveViewTitleFmt = "[fg:bg:b] %s([hilite:bg:b]%s[fg:bg:-])[fg:bg:-] " + yamlAction = "YAML" +) // LiveView represents a live text viewer. type LiveView struct { @@ -26,7 +31,7 @@ type LiveView struct { title string model model.ResourceViewer text *tview.TextView - actions ui.KeyActions + actions *ui.KeyActions app *App cmdBuff *model.FishBuff currentRegion, maxRegions int @@ -43,7 +48,7 @@ func NewLiveView(app *App, title string, m model.ResourceViewer) *LiveView { text: tview.NewTextView(), app: app, title: title, - actions: make(ui.KeyActions), + actions: ui.NewKeyActions(), currentRegion: 0, maxRegions: 0, cmdBuff: model.NewFishBuff('/', model.FilterBuffer), @@ -55,6 +60,9 @@ func NewLiveView(app *App, title string, m model.ResourceViewer) *LiveView { return &v } +func (v *LiveView) SetFilter(string) {} +func (v *LiveView) SetLabelFilter(map[string]string) {} + // Init initializes the viewer. func (v *LiveView) Init(_ context.Context) error { if v.title != "" { @@ -70,13 +78,16 @@ func (v *LiveView) Init(_ context.Context) error { v.app.Styles.AddListener(v) v.StylesChanged(v.app.Styles) + v.setFullScreen(v.app.Config.K9s.UI.DefaultsToFullScreen) v.app.Prompt().SetModel(v.cmdBuff) v.cmdBuff.AddListener(v) v.bindKeys() v.SetInputCapture(v.keyboard) - v.model.AddListener(v) + if v.model != nil { + v.model.AddListener(v) + } return nil } @@ -93,37 +104,17 @@ func (v *LiveView) ResourceFailed(err error) { v.text.SetText(cowTalk(err.Error(), x+w)) } -func (*LiveView) linesWithRegions(lines []string, matches fuzzy.Matches) []string { - ll := make([]string, len(lines)) - copy(ll, lines) - offsetForLine := make(map[int]int) - for i, m := range matches { - loc, line := m.MatchedIndexes, ll[m.Index] - offset := offsetForLine[m.Index] - loc[0], loc[1] = loc[0]+offset, loc[1]+offset - regionStr := `<<<"search_` + strconv.Itoa(i) + `">>>` + line[loc[0]:loc[1]] + `<<<"">>>` - ll[m.Index] = line[:loc[0]] + regionStr + line[loc[1]:] - offsetForLine[m.Index] += len(regionStr) - (loc[1] - loc[0]) - } - return ll -} - // ResourceChanged notifies when the filter changes. func (v *LiveView) ResourceChanged(lines []string, matches fuzzy.Matches) { v.app.QueueUpdateDraw(func() { - defer func(t time.Time) { - log.Debug().Msgf("Live view render time: %v", time.Since(t)) - }(time.Now()) - v.text.SetTextAlign(tview.AlignLeft) - v.maxRegions = len(matches) + v.currentRegion, v.maxRegions = 0, len(matches) if v.text.GetText(true) == "" { v.text.ScrollToBeginning() } - lines = v.linesWithRegions(lines, matches) - + lines = linesWithRegions(lines, matches) v.text.SetText(colorizeYAML(v.app.Styles.Views().Yaml, strings.Join(lines, "\n"))) v.text.Highlight() if v.currentRegion < v.maxRegions { @@ -148,7 +139,7 @@ func (v *LiveView) BufferActive(state bool, k model.BufferKind) { } func (v *LiveView) bindKeys() { - v.actions.Set(ui.KeyActions{ + v.actions.Bulk(ui.KeyMap{ tcell.KeyEnter: ui.NewSharedKeyAction("Filter", v.filterCmd, false), tcell.KeyEscape: ui.NewKeyAction("Back", v.resetCmd, false), tcell.KeyCtrlS: ui.NewKeyAction("Save", v.saveCmd, false), @@ -161,11 +152,41 @@ func (v *LiveView) bindKeys() { tcell.KeyDelete: ui.NewSharedKeyAction("Erase", v.eraseCmd, false), }) - if v.title == "YAML" { - v.actions.Add(ui.KeyActions{ - ui.KeyM: ui.NewKeyAction("Toggle ManagedFields", v.toggleManagedCmd, true), - }) + if !v.app.Config.K9s.IsReadOnly() { + v.actions.Add(ui.KeyE, ui.NewKeyAction("Edit", v.editCmd, true)) + } + if v.title == yamlAction { + v.actions.Add(ui.KeyM, ui.NewKeyAction("Toggle ManagedFields", v.toggleManagedCmd, true)) + } + if v.model != nil && v.model.GVR().IsDecodable() { + v.actions.Add(ui.KeyX, ui.NewKeyAction("Toggle Decode", v.toggleEncodedDecodedCmd, true)) + } +} + +func (v *LiveView) toggleEncodedDecodedCmd(evt *tcell.EventKey) *tcell.EventKey { + m, ok := v.model.(model.EncDecResourceViewer) + + if !ok { + return evt + } + + m.Toggle() + v.Start() + return nil +} + +func (v *LiveView) editCmd(evt *tcell.EventKey) *tcell.EventKey { + path := v.model.GetPath() + if path == "" { + return evt } + v.Stop() + defer v.Start() + if err := editRes(v.app, v.model.GVR(), path); err != nil { + v.app.Flash().Err(err) + } + + return nil } // ToggleRefreshCmd is used for pausing the refreshing of data on config map and secrets. @@ -183,7 +204,7 @@ func (v *LiveView) toggleRefreshCmd(evt *tcell.EventKey) *tcell.EventKey { } func (v *LiveView) keyboard(evt *tcell.EventKey) *tcell.EventKey { - if a, ok := v.actions[ui.AsKey(evt)]; ok { + if a, ok := v.actions.Get(ui.AsKey(evt)); ok { return a.Action(evt) } @@ -195,11 +216,10 @@ func (v *LiveView) StylesChanged(s *config.Styles) { v.SetBackgroundColor(v.app.Styles.BgColor()) v.text.SetTextColor(v.app.Styles.FgColor()) v.SetBorderFocusColor(v.app.Styles.Frame().Border.FocusColor.Color()) - v.ResourceChanged(v.model.Peek(), nil) } // Actions returns menu actions. -func (v *LiveView) Actions() ui.KeyActions { +func (v *LiveView) Actions() *ui.KeyActions { return v.actions } @@ -261,16 +281,20 @@ func (v *LiveView) toggleFullScreenCmd(evt *tcell.EventKey) *tcell.EventKey { return evt } - v.fullScreen = !v.fullScreen - v.SetFullScreen(v.fullScreen) - v.Box.SetBorder(!v.fullScreen) - if v.fullScreen { + v.setFullScreen(!v.fullScreen) + + return nil +} + +func (v *LiveView) setFullScreen(isFullScreen bool) { + v.fullScreen = isFullScreen + v.SetFullScreen(isFullScreen) + v.Box.SetBorder(!isFullScreen) + if isFullScreen { v.Box.SetBorderPadding(0, 0, 0, 0) } else { v.Box.SetBorderPadding(0, 0, 1, 1) } - - return nil } func (v *LiveView) nextCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -348,10 +372,11 @@ func (v *LiveView) resetCmd(evt *tcell.EventKey) *tcell.EventKey { } func (v *LiveView) saveCmd(evt *tcell.EventKey) *tcell.EventKey { - if path, err := saveYAML(v.app.Config.K9s.GetScreenDumpDir(), v.app.Config.K9s.CurrentContextDir(), v.title, v.text.GetText(true)); err != nil { + name := fmt.Sprintf("%s--%s", strings.Replace(v.model.GetPath(), "/", "-", 1), strings.ToLower(v.title)) + if _, err := saveYAML(v.app.Config.K9s.ContextScreenDumpDir(), name, sanitizeEsc(v.text.GetText(true))); err != nil { v.app.Flash().Err(err) } else { - v.app.Flash().Infof("Log %s saved successfully!", path) + v.app.Flash().Infof("File %q saved successfully!", name) } return nil @@ -361,7 +386,10 @@ func (v *LiveView) updateTitle() { if v.title == "" { return } - fmat := fmt.Sprintf(liveViewTitleFmt, v.title, v.model.GetPath()) + var fmat string + if v.model != nil { + fmat = fmt.Sprintf(liveViewTitleFmt, v.title, v.model.GetPath()) + } buff := v.cmdBuff.GetText() if buff == "" { diff --git a/internal/view/live_view_test.go b/internal/view/live_view_test.go index d3b801cc3e..923ed3a84d 100644 --- a/internal/view/live_view_test.go +++ b/internal/view/live_view_test.go @@ -1,64 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( - "strconv" + "context" "testing" - "github.com/sahilm/fuzzy" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" "github.com/stretchr/testify/assert" ) -func matchTag(i int, s string) string { - return `<<<"search_` + strconv.Itoa(i) + `">>>` + s + `<<<"">>>` -} +func TestLiveViewSetText(t *testing.T) { + s := ` +apiVersion: v1 + data: + the secret name you want to quote to use tls.","title":"secretName","type":"string"}},"required":["http","class","classInSpec"],"type":"object"} +` + + v := NewLiveView(NewApp(mock.NewMockConfig()), "fred", nil) + assert.NoError(t, v.Init(context.Background())) + v.text.SetText(colorizeYAML(config.Yaml{}, s)) -func TestLiveView_linesWithRegions(t *testing.T) { - uu := map[string]struct { - lines []string - matches fuzzy.Matches - e []string - }{ - "empty-lines": { - e: []string{}, - }, - "no-match": { - lines: []string{"bar"}, - e: []string{"bar"}, - }, - "single-match": { - lines: []string{"foo", "bar", "baz"}, - matches: fuzzy.Matches{ - {Index: 1, MatchedIndexes: []int{0, 3}}, - }, - e: []string{"foo", matchTag(0, "bar"), "baz"}, - }, - "multiple-matches": { - lines: []string{"foo", "bar", "baz"}, - matches: fuzzy.Matches{ - {Index: 1, MatchedIndexes: []int{0, 3}}, - {Index: 2, MatchedIndexes: []int{0, 3}}, - }, - e: []string{"foo", matchTag(0, "bar"), matchTag(1, "baz")}, - }, - "multiple-matches-same-line": { - lines: []string{"foosfoo baz", "dfbarfoos bar"}, - matches: fuzzy.Matches{ - {Index: 0, MatchedIndexes: []int{0, 3}}, - {Index: 0, MatchedIndexes: []int{4, 7}}, - {Index: 1, MatchedIndexes: []int{5, 8}}, - }, - e: []string{ - matchTag(0, "foo") + "s" + matchTag(1, "foo") + " baz", - "dfbar" + matchTag(2, "foo") + "s bar", - }, - }, - } - var v LiveView - for k := range uu { - u := uu[k] - t.Run(k, func(t *testing.T) { - t.Parallel() - assert.Equal(t, u.e, v.linesWithRegions(u.lines, u.matches)) - }) - } + assert.Equal(t, s, sanitizeEsc(v.text.GetText(true))) } diff --git a/internal/view/log.go b/internal/view/log.go index 3057c1490d..36902cc499 100644 --- a/internal/view/log.go +++ b/internal/view/log.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -13,6 +16,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/color" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" "github.com/derailed/k9s/internal/ui" @@ -58,6 +62,9 @@ func NewLog(gvr client.GVR, opts *dao.LogOptions) *Log { return &l } +func (l *Log) SetFilter(string) {} +func (l *Log) SetLabelFilter(map[string]string) {} + // Init initializes the viewer. func (l *Log) Init(ctx context.Context) (err error) { if l.app, err = extractApp(ctx); err != nil { @@ -236,7 +243,7 @@ func (l *Log) Stop() { func (l *Log) Name() string { return logTitle } func (l *Log) bindKeys() { - l.logs.Actions().Set(ui.KeyActions{ + l.logs.Actions().Bulk(ui.KeyMap{ ui.Key0: ui.NewKeyAction("tail", l.sinceCmd(-1), true), ui.Key1: ui.NewKeyAction("head", l.sinceCmd(0), true), ui.Key2: ui.NewKeyAction("1m", l.sinceCmd(60), true), @@ -257,9 +264,7 @@ func (l *Log) bindKeys() { ui.KeyC: ui.NewKeyAction("Copy", cpCmd(l.app.Flash(), l.logs.TextView), true), }) if l.model.HasDefaultContainer() { - l.logs.Actions().Set(ui.KeyActions{ - ui.KeyA: ui.NewKeyAction("Toggle AllContainers", l.toggleAllContainers, true), - }) + l.logs.Actions().Add(ui.KeyA, ui.NewKeyAction("Toggle AllContainers", l.toggleAllContainers, true)) } } @@ -403,7 +408,7 @@ func (l *Log) filterCmd(evt *tcell.EventKey) *tcell.EventKey { // SaveCmd dumps the logs to file. func (l *Log) SaveCmd(*tcell.EventKey) *tcell.EventKey { - path, err := saveData(l.app.Config.K9s.GetScreenDumpDir(), l.app.Config.K9s.CurrentContextDir(), l.model.GetPath(), l.logs.GetText(true)) + path, err := saveData(l.app.Config.K9s.ContextScreenDumpDir(), l.model.GetPath(), l.logs.GetText(true)) if err != nil { l.app.Flash().Err(err) return nil @@ -417,20 +422,17 @@ func ensureDir(dir string) error { return os.MkdirAll(dir, 0744) } -func saveData(screenDumpDir, context, fqn, data string) (string, error) { - dir := filepath.Join(screenDumpDir, context) +func saveData(dir, fqn, logs string) (string, error) { if err := ensureDir(dir); err != nil { return "", err } - now := time.Now().UnixNano() - fName := fmt.Sprintf("%s-%d.log", strings.Replace(fqn, "/", "-", 1), now) - - path := filepath.Join(dir, fName) + f := fmt.Sprintf("%s-%d.log", fqn, time.Now().UnixNano()) + path := filepath.Join(dir, data.SanitizeFileName(f)) mod := os.O_CREATE | os.O_WRONLY file, err := os.OpenFile(path, mod, 0600) if err != nil { - log.Error().Err(err).Msgf("LogFile create %s", path) + log.Error().Err(err).Msgf("Log file save failed: %q", path) return "", nil } defer func() { @@ -438,7 +440,7 @@ func saveData(screenDumpDir, context, fqn, data string) (string, error) { log.Error().Err(err).Msg("Closing Log file") } }() - if _, err := file.Write([]byte(data)); err != nil { + if _, err := file.WriteString(logs); err != nil { return "", err } @@ -452,7 +454,7 @@ func (l *Log) clearCmd(*tcell.EventKey) *tcell.EventKey { func (l *Log) markCmd(*tcell.EventKey) *tcell.EventKey { _, _, w, _ := l.GetRect() - fmt.Fprintf(l.ansiWriter, "\n[white:-:b]%s[-:-:-]", strings.Repeat("─", w-4)) + fmt.Fprintf(l.ansiWriter, "\n[%s:-:b]%s[-:-:-]", l.app.Styles.Views().Log.FgColor.String(), strings.Repeat("─", w-4)) l.follow = true return nil diff --git a/internal/view/log_indicator.go b/internal/view/log_indicator.go index 43aacff782..06e1d7f159 100644 --- a/internal/view/log_indicator.go +++ b/internal/view/log_indicator.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -6,7 +9,6 @@ import ( "github.com/derailed/k9s/internal/config" "github.com/derailed/tview" - "github.com/rs/zerolog/log" ) const spacer = " " @@ -33,7 +35,7 @@ func NewLogIndicator(cfg *config.Config, styles *config.Styles, allContainers bo TextView: tview.NewTextView(), indicator: make([]byte, 0, 100), scrollStatus: 1, - fullScreen: cfg.K9s.Logger.FullScreenLogs, + fullScreen: cfg.K9s.UI.DefaultsToFullScreen, textWrap: cfg.K9s.Logger.TextWrap, showTime: cfg.K9s.Logger.ShowTime, showJson: cfg.K9s.Logger.ShowJSON, @@ -171,7 +173,5 @@ func (l *LogIndicator) Refresh() { l.indicator = append(l.indicator, fmt.Sprintf(toggleOffFmt, "Wrap", "")...) } - log.Debug().Msgf("INDICATOR: %q", l.indicator) - _, _ = l.Write(l.indicator) } diff --git a/internal/view/log_indicator_test.go b/internal/view/log_indicator_test.go index 48db3eb156..26381eef9f 100644 --- a/internal/view/log_indicator_test.go +++ b/internal/view/log_indicator_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( diff --git a/internal/view/log_int_test.go b/internal/view/log_int_test.go index 6f51195765..0ea627aec0 100644 --- a/internal/view/log_int_test.go +++ b/internal/view/log_int_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( diff --git a/internal/view/log_test.go b/internal/view/log_test.go index d5c4cc8e49..689c9e8642 100644 --- a/internal/view/log_test.go +++ b/internal/view/log_test.go @@ -1,14 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( "bytes" + "errors" "fmt" + "io/fs" "os" - "path/filepath" "testing" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/view" @@ -106,10 +111,15 @@ func TestLogViewSave(t *testing.T) { ii.Lines(0, false, false, ll) v.Flush(ll) - dir := filepath.Join(app.Config.K9s.GetScreenDumpDir(), app.Config.K9s.CurrentCluster) - c1, _ := os.ReadDir(dir) + dd := "/tmp/test-dumps/na" + assert.NoError(t, ensureDumpDir(dd)) + app.Config.K9s.ScreenDumpDir = "/tmp/test-dumps" + dir := app.Config.K9s.ContextScreenDumpDir() + c1, err := os.ReadDir(dir) + assert.NoError(t, err, fmt.Sprintf("Dir: %q", dir)) v.SaveCmd(nil) - c2, _ := os.ReadDir(dir) + c2, err := os.ReadDir(dir) + assert.NoError(t, err, fmt.Sprintf("Dir: %q", dir)) assert.Equal(t, len(c2), len(c1)+1) } @@ -131,7 +141,7 @@ func TestAllContainerKeyBinding(t *testing.T) { t.Run(k, func(t *testing.T) { v := view.NewLog(client.NewGVR("v1/pods"), u.opts) assert.NoError(t, v.Init(makeContext())) - _, got := v.Logs().Actions()[ui.KeyA] + _, got := v.Logs().Actions().Get(ui.KeyA) assert.Equal(t, u.e, got) }) } @@ -141,5 +151,16 @@ func TestAllContainerKeyBinding(t *testing.T) { // Helpers... func makeApp() *view.App { - return view.NewApp(config.NewConfig(ks{})) + return view.NewApp(mock.NewMockConfig()) +} + +func ensureDumpDir(n string) error { + config.AppDumpsDir = n + if _, err := os.Stat(n); errors.Is(err, fs.ErrNotExist) { + return os.MkdirAll(n, 0700) + } + if err := os.RemoveAll(n); err != nil { + return err + } + return os.MkdirAll(n, 0700) } diff --git a/internal/view/logger.go b/internal/view/logger.go index 19446dea65..7fc649f0ec 100644 --- a/internal/view/logger.go +++ b/internal/view/logger.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -14,7 +17,7 @@ import ( type Logger struct { *tview.TextView - actions ui.KeyActions + actions *ui.KeyActions app *App title, subject string cmdBuff *model.FishBuff @@ -25,7 +28,7 @@ func NewLogger(app *App) *Logger { return &Logger{ TextView: tview.NewTextView(), app: app, - actions: make(ui.KeyActions), + actions: ui.NewKeyActions(), cmdBuff: model.NewFishBuff('/', model.FilterBuffer), } } @@ -66,7 +69,7 @@ func (l *Logger) BufferActive(state bool, k model.BufferKind) { } func (l *Logger) bindKeys() { - l.actions.Set(ui.KeyActions{ + l.actions.Bulk(ui.KeyMap{ tcell.KeyEscape: ui.NewKeyAction("Back", l.resetCmd, false), tcell.KeyCtrlS: ui.NewKeyAction("Save", l.saveCmd, false), ui.KeyC: ui.NewKeyAction("Copy", cpCmd(l.app.Flash(), l.TextView), true), @@ -76,7 +79,7 @@ func (l *Logger) bindKeys() { } func (l *Logger) keyboard(evt *tcell.EventKey) *tcell.EventKey { - if a, ok := l.actions[ui.AsKey(evt)]; ok { + if a, ok := l.actions.Get(ui.AsKey(evt)); ok { return a.Action(evt) } @@ -96,7 +99,7 @@ func (l *Logger) SetSubject(s string) { } // Actions returns menu actions. -func (l *Logger) Actions() ui.KeyActions { +func (l *Logger) Actions() *ui.KeyActions { return l.actions } @@ -151,7 +154,7 @@ func (l *Logger) resetCmd(evt *tcell.EventKey) *tcell.EventKey { } func (l *Logger) saveCmd(evt *tcell.EventKey) *tcell.EventKey { - if path, err := saveYAML(l.app.Config.K9s.GetScreenDumpDir(), l.app.Config.K9s.CurrentContextDir(), l.title, l.GetText(true)); err != nil { + if path, err := saveYAML(l.app.Config.K9s.ContextScreenDumpDir(), l.title, l.GetText(true)); err != nil { l.app.Flash().Err(err) } else { l.app.Flash().Infof("Log %s saved successfully!", path) diff --git a/internal/view/logs_extender.go b/internal/view/logs_extender.go index b31b80f426..589283ea6e 100644 --- a/internal/view/logs_extender.go +++ b/internal/view/logs_extender.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -5,6 +8,8 @@ import ( "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // LogsExtender adds log actions to a given viewer. @@ -26,8 +31,8 @@ func NewLogsExtender(v ResourceViewer, f LogOptionsFunc) ResourceViewer { } // BindKeys injects new menu actions. -func (l *LogsExtender) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (l *LogsExtender) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyL: ui.NewKeyAction("Logs", l.logsCmd(false), true), ui.KeyP: ui.NewKeyAction("Logs Previous", l.logsCmd(true), true), }) @@ -55,7 +60,7 @@ func isResourcePath(p string) bool { func (l *LogsExtender) showLogs(path string, prev bool) { ns, _ := client.Namespaced(path) - _, err := l.App().factory.CanForResource(ns, "v1/pods", client.MonitorAccess) + _, err := l.App().factory.CanForResource(ns, "v1/pods", client.ListAccess) if err != nil { l.App().Flash().Err(err) return @@ -89,3 +94,27 @@ func (l *LogsExtender) buildLogOpts(path, co string, prevLogs bool) *dao.LogOpti return &opts } + +func podLogOptions(app *App, fqn string, prev bool, m metav1.ObjectMeta, spec v1.PodSpec) *dao.LogOptions { + var ( + cc = fetchContainers(m, spec, true) + cfg = app.Config.K9s.Logger + opts = dao.LogOptions{ + Path: fqn, + Lines: int64(cfg.TailCount), + SinceSeconds: cfg.SinceSeconds, + SingleContainer: len(cc) == 1, + ShowTimestamp: cfg.ShowTime, + Previous: prev, + } + ) + if c, ok := dao.GetDefaultContainer(m, spec); ok { + opts.Container, opts.DefaultContainer = c, c + } else if len(cc) == 1 { + opts.Container = cc[0] + } else { + opts.AllContainers = true + } + + return &opts +} diff --git a/internal/view/node.go b/internal/view/node.go index d70b2b517b..31a2001540 100644 --- a/internal/view/node.go +++ b/internal/view/node.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -36,42 +39,64 @@ func (n *Node) nodeContext(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeyPodCounting, !n.App().Config.K9s.DisablePodCounting) } -func (n *Node) bindDangerousKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ - ui.KeyC: ui.NewKeyAction("Cordon", n.toggleCordonCmd(true), true), - ui.KeyU: ui.NewKeyAction("Uncordon", n.toggleCordonCmd(false), true), - ui.KeyR: ui.NewKeyAction("Drain", n.drainCmd, true), +func (n *Node) bindDangerousKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ + ui.KeyC: ui.NewKeyActionWithOpts( + "Cordon", + n.toggleCordonCmd(true), + ui.ActionOpts{ + Visible: true, + Dangerous: true, + }, + ), + ui.KeyU: ui.NewKeyActionWithOpts( + "Uncordon", + n.toggleCordonCmd(false), + ui.ActionOpts{ + Visible: true, + Dangerous: true, + }, + ), + ui.KeyR: ui.NewKeyActionWithOpts( + "Drain", + n.drainCmd, + ui.ActionOpts{ + Visible: true, + Dangerous: true, + }, + ), }) - cl := n.App().Config.K9s.CurrentCluster - if n.App().Config.K9s.Clusters[cl].FeatureGates.NodeShell { - aa.Add(ui.KeyActions{ - ui.KeyS: ui.NewKeyAction("Shell", n.sshCmd, true), - }) + ct, err := n.App().Config.K9s.ActiveContext() + if err != nil { + log.Error().Err(err).Msgf("No active context located") + return + } + if ct.FeatureGates.NodeShell { + aa.Add(ui.KeyS, ui.NewKeyAction("Shell", n.sshCmd, true)) } } -func (n *Node) bindKeys(aa ui.KeyActions) { - aa.Delete(ui.KeySpace, tcell.KeyCtrlSpace) - +func (n *Node) bindKeys(aa *ui.KeyActions) { if !n.App().Config.K9s.IsReadOnly() { n.bindDangerousKeys(aa) } - aa.Add(ui.KeyActions{ - ui.KeyY: ui.NewKeyAction("YAML", n.yamlCmd, true), + aa.Bulk(ui.KeyMap{ + ui.KeyY: ui.NewKeyAction(yamlAction, n.yamlCmd, true), + ui.KeyShiftR: ui.NewKeyAction("Sort ROLE", n.GetTable().SortColCmd("ROLE", true), false), ui.KeyShiftC: ui.NewKeyAction("Sort CPU", n.GetTable().SortColCmd(cpuCol, false), false), ui.KeyShiftM: ui.NewKeyAction("Sort MEM", n.GetTable().SortColCmd(memCol, false), false), - ui.KeyShift0: ui.NewKeyAction("Sort Pods", n.GetTable().SortColCmd("PODS", false), false), + ui.KeyShiftO: ui.NewKeyAction("Sort Pods", n.GetTable().SortColCmd("PODS", false), false), }) } -func (n *Node) showPods(a *App, _ ui.Tabular, _, path string) { - showPods(a, n.GetTable().GetSelectedItem(), client.AllNamespaces, "spec.nodeName="+path) +func (n *Node) showPods(a *App, _ ui.Tabular, _ client.GVR, path string) { + showPods(a, n.GetTable().GetSelectedItem(), client.BlankNamespace, "spec.nodeName="+path) } func (n *Node) drainCmd(evt *tcell.EventKey) *tcell.EventKey { - path := n.GetTable().GetSelectedItem() - if path == "" { + sels := n.GetTable().GetSelectedItems() + if len(sels) == 0 { return evt } @@ -79,12 +104,12 @@ func (n *Node) drainCmd(evt *tcell.EventKey) *tcell.EventKey { GracePeriodSeconds: -1, Timeout: 5 * time.Second, } - ShowDrain(n, path, opts, drainNode) + ShowDrain(n, sels, opts, drainNode) return nil } -func drainNode(v ResourceViewer, path string, opts dao.DrainOptions) { +func drainNode(v ResourceViewer, sels []string, opts dao.DrainOptions) { res, err := dao.AccessorFor(v.App().factory, v.GVR()) if err != nil { v.App().Flash().Err(err) @@ -99,14 +124,14 @@ func drainNode(v ResourceViewer, path string, opts dao.DrainOptions) { v.Stop() defer v.Start() { - d := NewDetails(v.App(), "Drain Progress", path, true) + d := NewDetails(v.App(), "Drain Progress", "nodes", contentYAML, true) if err := v.App().inject(d, false); err != nil { v.App().Flash().Err(err) } - - if err := m.Drain(path, opts, d.GetWriter()); err != nil { - v.App().Flash().Err(err) - return + for _, sel := range sels { + if err := m.Drain(sel, opts, d.GetWriter()); err != nil { + v.App().Flash().Err(err) + } } v.Refresh() } @@ -114,8 +139,8 @@ func drainNode(v ResourceViewer, path string, opts dao.DrainOptions) { func (n *Node) toggleCordonCmd(cordon bool) func(evt *tcell.EventKey) *tcell.EventKey { return func(evt *tcell.EventKey) *tcell.EventKey { - path := n.GetTable().GetSelectedItem() - if path == "" { + sels := n.GetTable().GetSelectedItems() + if len(sels) == 0 { return evt } @@ -125,7 +150,11 @@ func (n *Node) toggleCordonCmd(cordon bool) func(evt *tcell.EventKey) *tcell.Eve } else { title, msg = title+"Uncordon", "Uncordon " } - msg += path + "?" + if len(sels) == 1 { + msg += sels[0] + "?" + } else { + msg += fmt.Sprintf("(%d) marked %s?", len(sels), n.GVR().R()) + } dialog.ShowConfirm(n.App().Styles.Dialog(), n.App().Content.Pages, title, msg, func() { res, err := dao.AccessorFor(n.App().factory, n.GVR()) if err != nil { @@ -137,8 +166,10 @@ func (n *Node) toggleCordonCmd(cordon bool) func(evt *tcell.EventKey) *tcell.Eve n.App().Flash().Err(fmt.Errorf("expecting a maintainer for %q", n.GVR())) return } - if err := m.ToggleCordon(path, cordon); err != nil { - n.App().Flash().Err(err) + for _, s := range sels { + if err := m.ToggleCordon(s, cordon); err != nil { + n.App().Flash().Err(err) + } } n.Refresh() }, func() {}) @@ -156,9 +187,7 @@ func (n *Node) sshCmd(evt *tcell.EventKey) *tcell.EventKey { n.Stop() defer n.Start() _, node := client.Namespaced(path) - if err := ssh(n.App(), node); err != nil { - log.Error().Err(err).Msgf("Node Shell Failed") - } + launchNodeShell(n, n.App(), node) return nil } @@ -193,7 +222,7 @@ func (n *Node) yamlCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - details := NewDetails(n.App(), "YAML", sel, true).Update(raw) + details := NewDetails(n.App(), yamlAction, sel, contentYAML, true).Update(raw) if err := n.App().inject(details, false); err != nil { n.App().Flash().Err(err) } diff --git a/internal/view/ns.go b/internal/view/ns.go index 0b99de0e15..1eac09dbef 100644 --- a/internal/view/ns.go +++ b/internal/view/ns.go @@ -1,12 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" - "github.com/rs/zerolog/log" ) const ( @@ -31,14 +32,14 @@ func NewNamespace(gvr client.GVR) ResourceViewer { return &n } -func (n *Namespace) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (n *Namespace) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyU: ui.NewKeyAction("Use", n.useNsCmd, true), ui.KeyShiftS: ui.NewKeyAction("Sort Status", n.GetTable().SortColCmd(statusCol, true), false), }) } -func (n *Namespace) switchNs(app *App, model ui.Tabular, gvr, path string) { +func (n *Namespace) switchNs(app *App, _ ui.Tabular, _ client.GVR, path string) { n.useNamespace(path) app.gotoResource("pods", "", false) } @@ -55,6 +56,9 @@ func (n *Namespace) useNsCmd(evt *tcell.EventKey) *tcell.EventKey { func (n *Namespace) useNamespace(fqn string) { _, ns := client.Namespaced(fqn) + if client.CleanseNamespace(n.App().Config.ActiveNamespace()) == ns { + return + } if err := n.App().switchNS(ns); err != nil { n.App().Flash().Err(err) return @@ -63,39 +67,39 @@ func (n *Namespace) useNamespace(fqn string) { n.App().Flash().Err(err) return } - - n.App().Flash().Infof("Namespace %s is now active!", ns) - if err := n.App().Config.Save(); err != nil { - log.Error().Err(err).Msg("Config file save failed!") - } } -func (n *Namespace) decorate(data *render.TableData) { - if n.App().Conn() == nil || len(data.RowEvents) == 0 { +func (n *Namespace) decorate(td *model1.TableData) { + if n.App().Conn() == nil || td.RowCount() == 0 { return } - // checks if all ns is in the list if not add it. - if _, ok := data.RowEvents.FindIndex(client.NamespaceAll); !ok { - data.RowEvents = append(data.RowEvents, - render.RowEvent{ - Kind: render.EventUnchanged, - Row: render.Row{ - ID: client.NamespaceAll, - Fields: render.Fields{client.NamespaceAll, "Active", "", "", ""}, - }, + if _, ok := td.FindRow(client.NamespaceAll); !ok { + td.AddRow(model1.RowEvent{ + Kind: model1.EventUnchanged, + Row: model1.Row{ + ID: client.NamespaceAll, + Fields: model1.Fields{client.NamespaceAll, "Active", "", "", ""}, }, + }, ) } - for _, re := range data.RowEvents { - if config.InList(n.App().Config.FavNamespaces(), re.Row.ID) { + favs := make(map[string]struct{}) + for _, ns := range n.App().Config.FavNamespaces() { + favs[ns] = struct{}{} + } + ans := n.App().Config.ActiveNamespace() + td.RowsRange(func(i int, re model1.RowEvent) bool { + _, n := client.Namespaced(re.Row.ID) + if _, ok := favs[n]; ok { re.Row.Fields[0] += favNSIndicator - re.Kind = render.EventUnchanged } - if n.App().Config.ActiveNamespace() == re.Row.ID { + if ans == re.Row.ID { re.Row.Fields[0] += defaultNSIndicator - re.Kind = render.EventUnchanged } - } + re.Kind = model1.EventUnchanged + td.SetRow(i, re) + return true + }) } diff --git a/internal/view/ns_test.go b/internal/view/ns_test.go index 093e2e57ed..3c06101feb 100644 --- a/internal/view/ns_test.go +++ b/internal/view/ns_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( diff --git a/internal/view/ofaas.go b/internal/view/ofaas.go deleted file mode 100644 index 5925a2e8f2..0000000000 --- a/internal/view/ofaas.go +++ /dev/null @@ -1,46 +0,0 @@ -package view - -// BOZO!! revamp with latest... -// import ( -// "strings" - -// "github.com/derailed/k9s/internal/client" -// "github.com/derailed/k9s/internal/render" -// "github.com/derailed/k9s/internal/ui" -// ) - -// // OpenFaas represents an OpenFaaS viewer. -// type OpenFaas struct { -// ResourceViewer -// } - -// // NewOpenFaas returns a new viewer. -// func NewOpenFaas(gvr client.GVR) ResourceViewer { -// o := OpenFaas{ResourceViewer: NewBrowser(gvr)} -// o.AddBindKeysFn(o.bindKeys) -// o.GetTable().SetEnterFn(o.showPods) - -// return &o -// } - -// func (o *OpenFaas) bindKeys(aa ui.KeyActions) { -// aa.Add(ui.KeyActions{ -// ui.KeyShiftS: ui.NewKeyAction("Sort Status", o.GetTable().SortColCmd(statusCol, true), false), -// ui.KeyShiftI: ui.NewKeyAction("Sort Invocations", o.GetTable().SortColCmd("INVOCATIONS", false), false), -// ui.KeyShiftR: ui.NewKeyAction("Sort Replicas", o.GetTable().SortColCmd("REPLICAS", false), false), -// ui.KeyShiftL: ui.NewKeyAction("Sort Available", o.GetTable().SortColCmd(availCol, false), false), -// }) -// } - -// func (o *OpenFaas) showPods(a *App, _ ui.Tabular, _, path string) { -// labels := o.GetTable().GetSelectedCell(o.GetTable().NameColIndex() + 3) -// sels := make(map[string]string) - -// tokens := strings.Split(labels, ",") -// for _, t := range tokens { -// s := strings.Split(t, "=") -// sels[s[0]] = s[1] -// } - -// showPodsWithLabels(a, path, sels) -// } diff --git a/internal/view/owner_extender.go b/internal/view/owner_extender.go new file mode 100644 index 0000000000..89b42449ec --- /dev/null +++ b/internal/view/owner_extender.go @@ -0,0 +1,125 @@ +package view + +import ( + "context" + "fmt" + "github.com/derailed/k9s/internal/ui/dialog" + "github.com/rs/zerolog/log" + + "github.com/derailed/tcell/v2" + "github.com/go-errors/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/ui" +) + +// OwnerExtender adds owner actions to a given viewer. +type OwnerExtender struct { + ResourceViewer +} + +// NewOwnerExtender returns a new extender. +func NewOwnerExtender(r ResourceViewer) ResourceViewer { + v := &OwnerExtender{ResourceViewer: r} + v.AddBindKeysFn(v.bindKeys) + + return v +} + +func (v *OwnerExtender) bindKeys(aa *ui.KeyActions) { + aa.Add(ui.KeyShiftJ, ui.NewKeyAction("Jump Owner", v.ownerCmd, true)) +} + +func (v *OwnerExtender) ownerCmd(evt *tcell.EventKey) *tcell.EventKey { + path := v.GetTable().GetSelectedItem() + if path == "" { + return evt + } + + if err := v.findOwnerFor(path); err != nil { + log.Warn().Msgf("Unable to jump to the owner of resource %q: %s", path, err) + v.App().Flash().Warnf("Unable to jump owner: %s", err) + } + return nil +} + +func (v *OwnerExtender) findOwnerFor(path string) error { + res, err := dao.AccessorFor(v.App().factory, v.GVR()) + if err != nil { + return err + } + + o, err := res.Get(v.defaultCtx(), path) + if err != nil { + return err + } + + u, ok := v.asUnstructuredObject(o) + if !ok { + return errors.Errorf("unsupported object type: %t", o) + } + + ns, _ := client.Namespaced(path) + ownerReferences := u.GetOwnerReferences() + if len(ownerReferences) == 1 { + return v.jumpOwner(ns, ownerReferences[0]) + } else if len(ownerReferences) > 1 { + owners := make([]string, 0, len(ownerReferences)) + for idx, ownerRef := range ownerReferences { + owners = append(owners, fmt.Sprintf("%d: %s", idx, ownerRef.Kind)) + } + + dialog.ShowSelection(v.App().Styles.Dialog(), v.App().Content.Pages, "Jump To", owners, func(index int) { + if index >= 0 { + err = v.jumpOwner(ns, ownerReferences[index]) + } + }) + return err + } + + return errors.Errorf("no owner found") +} + +func (v *OwnerExtender) jumpOwner(ns string, owner metav1.OwnerReference) error { + gv, err := schema.ParseGroupVersion(owner.APIVersion) + if err != nil { + return err + } + + gvr, namespaced, found := dao.MetaAccess.GVK2GVR(gv, owner.Kind) + if !found { + return errors.Errorf("unsupported GVK: %s/%s", owner.APIVersion, owner.Kind) + } + + var ownerFQN string + if namespaced { + ownerFQN = client.FQN(ns, owner.Name) + } else { + ownerFQN = owner.Name + } + + v.App().gotoResource(gvr.String(), ownerFQN, false) + return nil +} + +func (v *OwnerExtender) defaultCtx() context.Context { + return context.WithValue(context.Background(), internal.KeyFactory, v.App().factory) +} + +func (v *OwnerExtender) asUnstructuredObject(o runtime.Object) (*unstructured.Unstructured, bool) { + switch v := o.(type) { + case *unstructured.Unstructured: + return v, true + case *render.PodWithMetrics: + return v.Raw, true + default: + return nil, false + } +} diff --git a/internal/view/page_stack.go b/internal/view/page_stack.go index b7c4d80c96..03721e9573 100644 --- a/internal/view/page_stack.go +++ b/internal/view/page_stack.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( diff --git a/internal/view/pf.go b/internal/view/pf.go index c4063b9674..a92a1824ed 100644 --- a/internal/view/pf.go +++ b/internal/view/pf.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -41,13 +44,17 @@ func NewPortForward(gvr client.GVR) ResourceViewer { } func (p *PortForward) portForwardContext(ctx context.Context) context.Context { - return context.WithValue(ctx, internal.KeyBenchCfg, p.App().BenchFile) + if bc := p.App().BenchFile; bc != "" { + return context.WithValue(ctx, internal.KeyBenchCfg, p.App().BenchFile) + } + + return ctx } -func (p *PortForward) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (p *PortForward) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ tcell.KeyEnter: ui.NewKeyAction("View Benchmarks", p.showBenchCmd, true), - tcell.KeyCtrlL: ui.NewKeyAction("Benchmark Run/Stop", p.toggleBenchCmd, true), + ui.KeyB: ui.NewKeyAction("Benchmark Run/Stop", p.toggleBenchCmd, true), tcell.KeyCtrlD: ui.NewKeyAction("Delete", p.deleteCmd, true), ui.KeyShiftP: ui.NewKeyAction("Sort Ports", p.GetTable().SortColCmd("PORTS", true), false), ui.KeyShiftU: ui.NewKeyAction("Sort URL", p.GetTable().SortColCmd("URL", true), false), @@ -105,16 +112,25 @@ func (p *PortForward) toggleBenchCmd(evt *tcell.EventKey) *tcell.EventKey { } p.App().Status(model.FlashWarn, "Benchmark in progress...") - go p.runBenchmark() + go func() { + if err := p.runBenchmark(); err != nil { + log.Error().Err(err).Msgf("Benchmark run failed") + } + }() return nil } -func (p *PortForward) runBenchmark() { +func (p *PortForward) runBenchmark() error { log.Debug().Msg("Bench starting...") - p.bench.Run(p.App().Config.K9s.CurrentCluster, func() { - log.Debug().Msg("Bench Completed!") + ct, err := p.App().Config.K9s.ActiveContext() + if err != nil { + return err + } + name := p.App().Config.K9s.ActiveContextName() + p.bench.Run(ct.ClusterName, name, func() { + log.Debug().Msgf("Benchmark %q Completed!", name) p.App().QueueUpdate(func() { if p.bench.Canceled() { p.App().Status(model.FlashInfo, "Benchmark canceled") @@ -129,6 +145,8 @@ func (p *PortForward) runBenchmark() { }() }) }) + + return nil } func (p *PortForward) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/view/pf_dialog.go b/internal/view/pf_dialog.go index 134fed724f..27f0d29b0e 100644 --- a/internal/view/pf_dialog.go +++ b/internal/view/pf_dialog.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -30,7 +33,11 @@ func ShowPortForwards(v ResourceViewer, path string, ports port.ContainerPortSpe SetFieldTextColor(styles.FieldFgColor.Color()). SetFieldBackgroundColor(styles.BgColor.Color()) - address := v.App().Config.CurrentCluster().PortForwardAddress + ct, err := v.App().Config.K9s.ActiveContext() + if err != nil { + log.Error().Err(err).Msgf("No active context detected") + return + } pf, err := aa.PreferredPorts(ports) if err != nil { @@ -54,6 +61,7 @@ func ShowPortForwards(v ResourceViewer, path string, ports port.ContainerPortSpe if loField.GetText() == "" { loField.SetPlaceholder("Enter a local port") } + address := ct.PortForwardAddress f.AddInputField("Address:", address, fieldLen, nil, func(h string) { address = h }) diff --git a/internal/view/pf_dialog_test.go b/internal/view/pf_dialog_test.go index e59ccaec7a..e38fcb12f5 100644 --- a/internal/view/pf_dialog_test.go +++ b/internal/view/pf_dialog_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( diff --git a/internal/view/pf_extender.go b/internal/view/pf_extender.go index 42120c127a..2e19cc2694 100644 --- a/internal/view/pf_extender.go +++ b/internal/view/pf_extender.go @@ -1,9 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( + "context" "fmt" "strings" + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/port" "github.com/derailed/k9s/internal/ui" @@ -30,8 +36,9 @@ func NewPortForwardExtender(r ResourceViewer) ResourceViewer { return &p } -func (p *PortForwardExtender) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (p *PortForwardExtender) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ + ui.KeyF: ui.NewKeyAction("Show PortForward", p.showPFCmd, true), ui.KeyShiftF: ui.NewKeyAction("Port-Forward", p.portFwdCmd, true), }) } @@ -58,6 +65,32 @@ func (p *PortForwardExtender) portFwdCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } +func (p *PortForwardExtender) showPFCmd(evt *tcell.EventKey) *tcell.EventKey { + path := p.GetTable().GetSelectedItem() + if path == "" { + return evt + } + + podName, err := p.fetchPodName(path) + if err != nil { + p.App().Flash().Err(err) + return nil + } + + if !p.App().factory.Forwarders().IsPodForwarded(podName) { + p.App().Flash().Errf("no port-forward defined") + return nil + } + + pf := NewPortForward(client.NewGVR("portforwards")) + pf.SetContextFn(p.portForwardContext) + if err := p.App().inject(pf, false); err != nil { + p.App().Flash().Err(err) + } + + return nil +} + func (p *PortForwardExtender) fetchPodName(path string) (string, error) { res, err := dao.AccessorFor(p.App().factory, p.GVR()) if err != nil { @@ -71,6 +104,14 @@ func (p *PortForwardExtender) fetchPodName(path string) (string, error) { return ctrl.Pod(path) } +func (p *PortForwardExtender) portForwardContext(ctx context.Context) context.Context { + if bc := p.App().BenchFile; bc != "" { + ctx = context.WithValue(ctx, internal.KeyBenchCfg, p.App().BenchFile) + } + + return context.WithValue(ctx, internal.KeyPath, p.GetTable().GetSelectedItem()) +} + // ---------------------------------------------------------------------------- // Helpers... @@ -96,11 +137,9 @@ func runForward(v ResourceViewer, pf watch.Forwarder, f *portforward.PortForward pf.SetActive(true) if err := f.ForwardPorts(); err != nil { v.App().Flash().Err(err) - return } - v.App().QueueUpdateDraw(func() { - v.App().factory.DeleteForwarder(pf.FQN()) + v.App().factory.DeleteForwarder(pf.ID()) pf.SetActive(false) }) } @@ -122,7 +161,7 @@ func startFwdCB(v ResourceViewer, path string, pts port.PortTunnels) error { } log.Debug().Msgf(">>> Starting port forward %q -- %#v", pf.ID(), pt) go runForward(v, pf, fwd) - tt = append(tt, pt.ContainerPort) + tt = append(tt, pt.LocalPort) } if len(tt) == 1 { v.App().Flash().Infof("PortForward activated %s", tt[0]) @@ -134,6 +173,10 @@ func startFwdCB(v ResourceViewer, path string, pts port.PortTunnels) error { } func showFwdDialog(v ResourceViewer, path string, cb PortForwardCB) error { + ct, err := v.App().Config.CurrentContext() + if err != nil { + return err + } mm, anns, err := fetchPodPorts(v.App().factory, path) if err != nil { return err @@ -153,7 +196,7 @@ func showFwdDialog(v ResourceViewer, path string, cb PortForwardCB) error { return err } - pts, err := pfs.ToTunnels(v.App().Config.CurrentCluster().PortForwardAddress, ports, port.IsPortFree) + pts, err := pfs.ToTunnels(ct.PortForwardAddress, ports, port.IsPortFree) if err != nil { return err } diff --git a/internal/view/pf_extender_test.go b/internal/view/pf_extender_test.go index c7cf4c2148..20a7214ef3 100644 --- a/internal/view/pf_extender_test.go +++ b/internal/view/pf_extender_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( diff --git a/internal/view/pf_test.go b/internal/view/pf_test.go index 505cc60b69..245e593933 100644 --- a/internal/view/pf_test.go +++ b/internal/view/pf_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( diff --git a/internal/view/picker.go b/internal/view/picker.go index c789a37037..ce39b6b945 100644 --- a/internal/view/picker.go +++ b/internal/view/picker.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -20,10 +23,13 @@ type Picker struct { func NewPicker() *Picker { return &Picker{ List: tview.NewList(), - actions: ui.KeyActions{}, + actions: *ui.NewKeyActions(), } } +func (p *Picker) SetFilter(string) {} +func (p *Picker) SetLabelFilter(map[string]string) {} + // Init initializes the view. func (p *Picker) Init(ctx context.Context) error { app, err := extractApp(ctx) @@ -32,7 +38,7 @@ func (p *Picker) Init(ctx context.Context) error { } pickerView := app.Styles.Views().Picker - p.actions[tcell.KeyEscape] = ui.NewKeyAction("Back", app.PrevCmd, true) + p.actions.Add(tcell.KeyEscape, ui.NewKeyAction("Back", app.PrevCmd, true)) p.SetBorder(true) p.SetMainTextColor(pickerView.MainColor.Color()) @@ -42,7 +48,7 @@ func (p *Picker) Init(ctx context.Context) error { p.SetTitle(" [aqua::b]Containers Picker ") p.SetInputCapture(func(evt *tcell.EventKey) *tcell.EventKey { - if a, ok := p.actions[evt.Key()]; ok { + if a, ok := p.actions.Get(evt.Key()); ok { a.Action(evt) evt = nil } diff --git a/internal/view/pod.go b/internal/view/pod.go index f1d7f38825..75e5a2fb04 100644 --- a/internal/view/pod.go +++ b/internal/view/pod.go @@ -1,9 +1,13 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( "context" "errors" "fmt" + "io/fs" "os" "strings" @@ -11,6 +15,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" @@ -25,12 +30,15 @@ import ( ) const ( - windowsOS = "windows" - powerShell = "powershell" - osSelector = "kubernetes.io/os" - osBetaSelector = "beta." + osSelector - trUpload = "Upload" - trDownload = "Download" + windowsOS = "windows" + powerShell = "powershell" + osSelector = "kubernetes.io/os" + osBetaSelector = "beta." + osSelector + trUpload = "Upload" + trDownload = "Download" + pfIndicator = "[orange::b]Ⓕ" + defaultTxRetries = 999 + magicPrompt = "Yes Please!" ) // Pod represents a pod viewer. @@ -42,8 +50,12 @@ type Pod struct { func NewPod(gvr client.GVR) ResourceViewer { var p Pod p.ResourceViewer = NewPortForwardExtender( - NewImageExtender( - NewLogsExtender(NewBrowser(gvr), p.logOptions), + NewOwnerExtender( + NewVulnerabilityExtender( + NewImageExtender( + NewLogsExtender(NewBrowser(gvr), p.logOptions), + ), + ), ), ) p.AddBindKeysFn(p.bindKeys) @@ -53,43 +65,77 @@ func NewPod(gvr client.GVR) ResourceViewer { return &p } -func (p *Pod) portForwardIndicator(data *render.TableData) { +func (p *Pod) portForwardIndicator(data *model1.TableData) { ff := p.App().factory.Forwarders() - col := data.IndexOfHeader("PF") - for _, re := range data.RowEvents { + defer decorateCpuMemHeaderRows(p.App(), data) + idx, ok := data.IndexOfHeader("PF") + if !ok { + return + } + + data.RowsRange(func(_ int, re model1.RowEvent) bool { if ff.IsPodForwarded(re.Row.ID) { - re.Row.Fields[col] = "[orange::b]Ⓕ" + re.Row.Fields[idx] = pfIndicator } - } - decorateCpuMemHeaderRows(p.App(), data) + return true + }) } -func (p *Pod) bindDangerousKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ - tcell.KeyCtrlK: ui.NewKeyAction("Kill", p.killCmd, true), - ui.KeyS: ui.NewKeyAction("Shell", p.shellCmd, true), - ui.KeyA: ui.NewKeyAction("Attach", p.attachCmd, true), - ui.KeyT: ui.NewKeyAction("Transfer", p.transferCmd, true), - ui.KeyZ: ui.NewKeyAction("Sanitize", p.sanitizeCmd, true), +func (p *Pod) bindDangerousKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ + tcell.KeyCtrlK: ui.NewKeyActionWithOpts( + "Kill", + p.killCmd, + ui.ActionOpts{ + Visible: true, + Dangerous: true, + }), + ui.KeyS: ui.NewKeyActionWithOpts( + "Shell", + p.shellCmd, + ui.ActionOpts{ + Visible: true, + Dangerous: true, + }), + ui.KeyA: ui.NewKeyActionWithOpts( + "Attach", + p.attachCmd, + ui.ActionOpts{ + Visible: true, + Dangerous: true, + }), + ui.KeyT: ui.NewKeyActionWithOpts( + "Transfer", + p.transferCmd, + ui.ActionOpts{ + Visible: true, + Dangerous: true, + }), + ui.KeyZ: ui.NewKeyActionWithOpts( + "Sanitize", + p.sanitizeCmd, + ui.ActionOpts{ + Visible: true, + Dangerous: true, + }), }) } -func (p *Pod) bindKeys(aa ui.KeyActions) { +func (p *Pod) bindKeys(aa *ui.KeyActions) { if !p.App().Config.K9s.IsReadOnly() { p.bindDangerousKeys(aa) } - aa.Add(ui.KeyActions{ - ui.KeyN: ui.NewKeyAction("Show Node", p.showNode, true), - ui.KeyF: ui.NewKeyAction("Show PortForward", p.showPFCmd, true), + aa.Bulk(ui.KeyMap{ + ui.KeyO: ui.NewKeyAction("Show Node", p.showNode, true), ui.KeyShiftR: ui.NewKeyAction("Sort Ready", p.GetTable().SortColCmd(readyCol, true), false), ui.KeyShiftT: ui.NewKeyAction("Sort Restart", p.GetTable().SortColCmd("RESTARTS", false), false), ui.KeyShiftS: ui.NewKeyAction("Sort Status", p.GetTable().SortColCmd(statusCol, true), false), ui.KeyShiftI: ui.NewKeyAction("Sort IP", p.GetTable().SortColCmd("IP", true), false), ui.KeyShiftO: ui.NewKeyAction("Sort Node", p.GetTable().SortColCmd("NODE", true), false), }) - aa.Add(resourceSorters(p.GetTable())) + aa.Merge(resourceSorters(p.GetTable())) } func (p *Pod) logOptions(prev bool) (*dao.LogOptions, error) { @@ -103,28 +149,10 @@ func (p *Pod) logOptions(prev bool) (*dao.LogOptions, error) { return nil, err } - cc, cfg := fetchContainers(pod.ObjectMeta, pod.Spec, true), p.App().Config.K9s.Logger - opts := dao.LogOptions{ - Path: path, - Lines: int64(cfg.TailCount), - SinceSeconds: cfg.SinceSeconds, - SingleContainer: len(cc) == 1, - ShowTimestamp: cfg.ShowTime, - ShowJSON: cfg.ShowJSON, - Previous: prev, - } - if c, ok := dao.GetDefaultContainer(pod.ObjectMeta, pod.Spec); ok { - opts.Container, opts.DefaultContainer = c, c - } else if len(cc) == 1 { - opts.Container = cc[0] - } else { - opts.AllContainers = true - } - - return &opts, nil + return podLogOptions(p.App(), path, prev, pod.ObjectMeta, pod.Spec), nil } -func (p *Pod) showContainers(app *App, model ui.Tabular, gvr, path string) { +func (p *Pod) showContainers(app *App, _ ui.Tabular, _ client.GVR, _ string) { co := NewContainer(client.NewGVR("containers")) co.SetContextFn(p.coContext) if err := app.inject(co, false); err != nil { @@ -162,30 +190,6 @@ func (p *Pod) showNode(evt *tcell.EventKey) *tcell.EventKey { return nil } -func (p *Pod) showPFCmd(evt *tcell.EventKey) *tcell.EventKey { - path := p.GetTable().GetSelectedItem() - if path == "" { - return evt - } - - if !p.App().factory.Forwarders().IsPodForwarded(path) { - p.App().Flash().Errf("no port-forward defined") - return nil - } - pf := NewPortForward(client.NewGVR("portforwards")) - pf.SetContextFn(p.portForwardContext) - if err := p.App().inject(pf, false); err != nil { - p.App().Flash().Err(err) - } - - return nil -} - -func (p *Pod) portForwardContext(ctx context.Context) context.Context { - ctx = context.WithValue(ctx, internal.KeyBenchCfg, p.App().BenchFile) - return context.WithValue(ctx, internal.KeyPath, p.GetTable().GetSelectedItem()) -} - func (p *Pod) killCmd(evt *tcell.EventKey) *tcell.EventKey { selections := p.GetTable().GetSelectedItems() if len(selections) == 0 { @@ -269,9 +273,8 @@ func (p *Pod) sanitizeCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - ack := "sanitize me pods!" - msg := fmt.Sprintf("Sanitize deletes all pods in completed/error state\nPlease enter [orange::b]%s[-::-] to proceed.", ack) - dialog.ShowConfirmAck(p.App().App, p.App().Content.Pages, ack, true, "Sanitize", msg, func() { + msg := fmt.Sprintf("Sanitize deletes all pods in completed/error state\nPlease enter [orange::b]%s[-::-] to proceed.", magicPrompt) + dialog.ShowConfirmAck(p.App().App, p.App().Content.Pages, magicPrompt, true, "Sanitize", msg, func() { ctx, cancel := context.WithTimeout(context.Background(), 5*p.App().Conn().Config().CallTimeout()) defer cancel() total, err := s.Sanitize(ctx, p.GetTable().GetModel().GetNamespace()) @@ -293,36 +296,38 @@ func (p *Pod) transferCmd(evt *tcell.EventKey) *tcell.EventKey { } ns, n := client.Namespaced(path) - ack := func(from, to, co string, download, no_preserve bool) bool { - local := to - if !download { - local = from + ack := func(args dialog.TransferArgs) bool { + local := args.To + if !args.Download { + local = args.From } - if _, err := os.Stat(local); !download && os.IsNotExist(err) { + if _, err := os.Stat(local); !args.Download && errors.Is(err, fs.ErrNotExist) { p.App().Flash().Err(err) return false } - args := make([]string, 0, 10) - args = append(args, "cp") - args = append(args, strings.TrimSpace(from)) - args = append(args, strings.TrimSpace(to)) - args = append(args, fmt.Sprintf("--no-preserve=%t", no_preserve)) - if co != "" { - args = append(args, "-c="+co) + opts := make([]string, 0, 10) + opts = append(opts, "cp") + opts = append(opts, strings.TrimSpace(args.From)) + opts = append(opts, strings.TrimSpace(args.To)) + opts = append(opts, fmt.Sprintf("--no-preserve=%t", args.NoPreserve)) + opts = append(opts, fmt.Sprintf("--retries=%d", args.Retries)) + if args.CO != "" { + opts = append(opts, "-c="+args.CO) } + opts = append(opts, fmt.Sprintf("--retries=%d", args.Retries)) - opts := shellOpts{ + cliOpts := shellOpts{ background: true, - args: args, + args: opts, } op := trUpload - if download { + if args.Download { op = trDownload } - fqn := path + ":" + co - if err := runK(p.App(), opts); err != nil { + fqn := path + ":" + args.CO + if err := runK(p.App(), cliOpts); err != nil { p.App().cowCmd(err.Error()) } else { p.App().Flash().Infof("%s successful on %s!", op, fqn) @@ -342,6 +347,7 @@ func (p *Pod) transferCmd(evt *tcell.EventKey) *tcell.EventKey { Message: "Download Files", Pod: fmt.Sprintf("%s/%s:", ns, n), Ack: ack, + Retries: defaultTxRetries, Cancel: func() {}, } dialog.ShowUploads(p.App().Styles.Dialog(), p.App().Content.Pages, opts) @@ -451,7 +457,7 @@ func buildShellArgs(cmd, path, co string, kcfg *string) []string { args := make([]string, 0, 15) args = append(args, cmd, "-it") ns, po := client.Namespaced(path) - if ns != client.AllNamespaces { + if ns != client.BlankNamespace { args = append(args, "-n", ns) } args = append(args, po) @@ -546,13 +552,13 @@ func osFromSelector(s map[string]string) (string, bool) { return os, ok } -func resourceSorters(t *Table) ui.KeyActions { - return ui.KeyActions{ +func resourceSorters(t *Table) *ui.KeyActions { + return ui.NewKeyActionsFromMap(ui.KeyMap{ ui.KeyShiftC: ui.NewKeyAction("Sort CPU", t.SortColCmd(cpuCol, false), false), ui.KeyShiftM: ui.NewKeyAction("Sort MEM", t.SortColCmd(memCol, false), false), ui.KeyShiftX: ui.NewKeyAction("Sort CPU/R", t.SortColCmd("%CPU/R", false), false), ui.KeyShiftZ: ui.NewKeyAction("Sort MEM/R", t.SortColCmd("%MEM/R", false), false), tcell.KeyCtrlX: ui.NewKeyAction("Sort CPU/L", t.SortColCmd("%CPU/L", false), false), tcell.KeyCtrlQ: ui.NewKeyAction("Sort MEM/L", t.SortColCmd("%MEM/L", false), false), - } + }) } diff --git a/internal/view/pod_int_test.go b/internal/view/pod_int_test.go index e359924bfb..3a84f7272f 100644 --- a/internal/view/pod_int_test.go +++ b/internal/view/pod_int_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( diff --git a/internal/view/pod_test.go b/internal/view/pod_test.go index 1f942588d7..23bdebaf74 100644 --- a/internal/view/pod_test.go +++ b/internal/view/pod_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( @@ -6,7 +9,7 @@ import ( "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/view" "github.com/stretchr/testify/assert" ) @@ -16,12 +19,12 @@ func TestPodNew(t *testing.T) { assert.Nil(t, po.Init(makeCtx())) assert.Equal(t, "Pods", po.Name()) - assert.Equal(t, 27, len(po.Hints())) + assert.Equal(t, 28, len(po.Hints())) } // Helpers... func makeCtx() context.Context { - cfg := config.NewConfig(ks{}) + cfg := mock.NewMockConfig() return context.WithValue(context.Background(), internal.KeyApp, view.NewApp(cfg)) } diff --git a/internal/view/policy.go b/internal/view/policy.go index 7b5c8b04fa..b412b388f5 100644 --- a/internal/view/policy.go +++ b/internal/view/policy.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -30,7 +33,7 @@ func NewPolicy(app *App, subject, name string) *Policy { subjectName: name, } p.AddBindKeysFn(p.bindKeys) - p.GetTable().SetSortCol(nameCol, false) + p.GetTable().SetSortCol("API-GROUP", false) p.SetContextFn(p.subjectCtx) p.GetTable().SetEnterFn(blankEnterFn) @@ -43,11 +46,11 @@ func (p *Policy) subjectCtx(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeySubjectName, p.subjectName) } -func (p *Policy) bindKeys(aa ui.KeyActions) { +func (p *Policy) bindKeys(aa *ui.KeyActions) { aa.Delete(ui.KeyShiftA, tcell.KeyCtrlSpace, ui.KeySpace) - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ ui.KeyShiftN: ui.NewKeyAction("Sort Name", p.GetTable().SortColCmd(nameCol, true), false), - ui.KeyShiftO: ui.NewKeyAction("Sort Group", p.GetTable().SortColCmd("GROUP", true), false), + ui.KeyShiftA: ui.NewKeyAction("Sort Api-Group", p.GetTable().SortColCmd("API-GROUP", true), false), ui.KeyShiftB: ui.NewKeyAction("Sort Binding", p.GetTable().SortColCmd("BINDING", true), false), }) } diff --git a/internal/view/popeye.go b/internal/view/popeye.go index 8a92eb6d36..e3c6355b38 100644 --- a/internal/view/popeye.go +++ b/internal/view/popeye.go @@ -1,113 +1,116 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view -import ( - "context" - "fmt" - "strconv" - "time" +// import ( +// "context" +// "fmt" +// "strconv" +// "time" - "github.com/derailed/k9s/internal" - "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/render" - "github.com/derailed/k9s/internal/ui" - "github.com/derailed/tcell/v2" -) +// "github.com/derailed/k9s/internal" +// "github.com/derailed/k9s/internal/client" +// "github.com/derailed/k9s/internal/render" +// "github.com/derailed/k9s/internal/ui" +// "github.com/derailed/tcell/v2" +// ) -// Popeye represents a sanitizer view. -type Popeye struct { - ResourceViewer -} +// // Popeye represents a sanitizer view. +// type Popeye struct { +// ResourceViewer +// } -// NewPopeye returns a new view. -func NewPopeye(gvr client.GVR) ResourceViewer { - p := Popeye{ - ResourceViewer: NewBrowser(gvr), - } - p.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen) - p.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorMediumSpringGreen).Attributes(tcell.AttrNone)) - p.GetTable().SetSortCol("SCORE%", true) - p.GetTable().SetDecorateFn(p.decorateRows) - p.AddBindKeysFn(p.bindKeys) +// // NewPopeye returns a new view. +// func NewPopeye(gvr client.GVR) ResourceViewer { +// p := Popeye{ +// ResourceViewer: NewBrowser(gvr), +// } +// p.GetTable().SetBorderFocusColor(tcell.ColorMediumSpringGreen) +// p.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorMediumSpringGreen).Attributes(tcell.AttrNone)) +// p.GetTable().SetSortCol("SCORE%", true) +// p.GetTable().SetDecorateFn(p.decorateRows) +// p.AddBindKeysFn(p.bindKeys) - return &p -} +// return &p +// } -// Init initializes the view. -func (p *Popeye) Init(ctx context.Context) error { - if err := p.ResourceViewer.Init(ctx); err != nil { - return err - } - p.GetTable().GetModel().SetRefreshRate(5 * time.Second) +// // Init initializes the view. +// func (p *Popeye) Init(ctx context.Context) error { +// if err := p.ResourceViewer.Init(ctx); err != nil { +// return err +// } +// p.GetTable().GetModel().SetRefreshRate(5 * time.Second) - return nil -} +// return nil +// } -func (p *Popeye) decorateRows(data *render.TableData) { - var sum int - for _, re := range data.RowEvents { - n, err := strconv.Atoi(re.Row.Fields[1]) - if err != nil { - continue - } - sum += n - } - score, letter := 0, render.NAValue - if len(data.RowEvents) > 0 { - score = sum / len(data.RowEvents) - letter = grade(score) - } - p.GetTable().Extras = fmt.Sprintf("Score %d -- %s", score, letter) -} +// func (p *Popeye) decorateRows(data *model1.TableData) { +// var sum int +// for _, re := range data.RowEvents { +// n, err := strconv.Atoi(re.Row.Fields[1]) +// if err != nil { +// continue +// } +// sum += n +// } +// score, letter := 0, render.NAValue +// if len(data.RowEvents) > 0 { +// score = sum / len(data.RowEvents) +// letter = grade(score) +// } +// p.GetTable().Extras = fmt.Sprintf("Score %d -- %s", score, letter) +// } -func (p *Popeye) bindKeys(aa ui.KeyActions) { - aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) - aa.Add(ui.KeyActions{ - tcell.KeyEnter: ui.NewKeyAction("Goto", p.gotoCmd, true), - ui.KeyShiftR: ui.NewKeyAction("Sort Resource", p.GetTable().SortColCmd("RESOURCE", true), false), - ui.KeyShiftS: ui.NewKeyAction("Sort Score", p.GetTable().SortColCmd("SCORE%", true), false), - ui.KeyShiftO: ui.NewKeyAction("Sort OK", p.GetTable().SortColCmd("OK", true), false), - ui.KeyShiftI: ui.NewKeyAction("Sort Info", p.GetTable().SortColCmd("INFO", true), false), - ui.KeyShiftW: ui.NewKeyAction("Sort Warning", p.GetTable().SortColCmd("WARNING", true), false), - ui.KeyShiftE: ui.NewKeyAction("Sort Error", p.GetTable().SortColCmd("ERROR", true), false), - }) -} +// func (p *Popeye) bindKeys(aa ui.KeyActions) { +// aa.Delete(ui.KeyShiftA, ui.KeyShiftN, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) +// aa.Add(ui.KeyActions{ +// tcell.KeyEnter: ui.NewKeyAction("Goto", p.gotoCmd, true), +// ui.KeyShiftR: ui.NewKeyAction("Sort Resource", p.GetTable().SortColCmd("RESOURCE", true), false), +// ui.KeyShiftS: ui.NewKeyAction("Sort Score", p.GetTable().SortColCmd("SCORE%", true), false), +// ui.KeyShiftO: ui.NewKeyAction("Sort OK", p.GetTable().SortColCmd("OK", true), false), +// ui.KeyShiftI: ui.NewKeyAction("Sort Info", p.GetTable().SortColCmd("INFO", true), false), +// ui.KeyShiftW: ui.NewKeyAction("Sort Warning", p.GetTable().SortColCmd("WARNING", true), false), +// ui.KeyShiftE: ui.NewKeyAction("Sort Error", p.GetTable().SortColCmd("ERROR", true), false), +// }) +// } -func (p *Popeye) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { - path := p.GetTable().GetSelectedItem() - if path == "" { - return evt - } - v := NewSanitizer(client.NewGVR("sanitizer")) - v.SetContextFn(sanitizerCtx(path)) - if err := p.App().inject(v, false); err != nil { - p.App().Flash().Err(err) - } +// func (p *Popeye) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { +// path := p.GetTable().GetSelectedItem() +// if path == "" { +// return evt +// } +// v := NewSanitizer(client.NewGVR("sanitizer")) +// v.SetContextFn(sanitizerCtx(path)) +// if err := p.App().inject(v, false); err != nil { +// p.App().Flash().Err(err) +// } - return nil -} +// return nil +// } -func sanitizerCtx(path string) ContextFunc { - return func(ctx context.Context) context.Context { - ctx = context.WithValue(ctx, internal.KeyPath, path) - return ctx - } -} +// func sanitizerCtx(path string) ContextFunc { +// return func(ctx context.Context) context.Context { +// ctx = context.WithValue(ctx, internal.KeyPath, path) +// return ctx +// } +// } -// Helpers... +// // Helpers... -func grade(score int) string { - switch { - case score >= 90: - return "A" - case score >= 80: - return "B" - case score >= 70: - return "C" - case score >= 60: - return "D" - case score >= 50: - return "E" - default: - return "F" - } -} +// func grade(score int) string { +// switch { +// case score >= 90: +// return "A" +// case score >= 80: +// return "B" +// case score >= 70: +// return "C" +// case score >= 60: +// return "D" +// case score >= 50: +// return "E" +// default: +// return "F" +// } +// } diff --git a/internal/view/priorityclass.go b/internal/view/priorityclass.go index 4325bca8d1..7f9ef90908 100644 --- a/internal/view/priorityclass.go +++ b/internal/view/priorityclass.go @@ -1,7 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" ) @@ -21,12 +25,10 @@ func NewPriorityClass(gvr client.GVR) ResourceViewer { return &s } -func (s *PriorityClass) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ - ui.KeyU: ui.NewKeyAction("UsedBy", s.refCmd, true), - }) +func (s *PriorityClass) bindKeys(aa *ui.KeyActions) { + aa.Add(ui.KeyU, ui.NewKeyAction("UsedBy", s.refCmd, true)) } func (s *PriorityClass) refCmd(evt *tcell.EventKey) *tcell.EventKey { - return scanRefs(evt, s.App(), s.GetTable(), "scheduling.k8s.io/v1/priorityclasses") + return scanRefs(evt, s.App(), s.GetTable(), dao.PcGVR) } diff --git a/internal/view/priorityclass_test.go b/internal/view/priorityclass_test.go index 1c258fb9f4..60cef2abf3 100644 --- a/internal/view/priorityclass_test.go +++ b/internal/view/priorityclass_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( diff --git a/internal/view/pulse.go b/internal/view/pulse.go index 1f060142aa..6d36e7a5fd 100644 --- a/internal/view/pulse.go +++ b/internal/view/pulse.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -61,7 +64,7 @@ type Pulse struct { gvr client.GVR model *model.Pulse cancelFn context.CancelFunc - actions ui.KeyActions + actions *ui.KeyActions charts []Graphable } @@ -70,10 +73,13 @@ func NewPulse(gvr client.GVR) ResourceViewer { return &Pulse{ Grid: tview.NewGrid(), model: model.NewPulse(gvr.String()), - actions: make(ui.KeyActions), + actions: ui.NewKeyActions(), } } +func (p *Pulse) SetFilter(string) {} +func (p *Pulse) SetLabelFilter(map[string]string) {} + // Init initializes the view. func (p *Pulse) Init(ctx context.Context) error { p.SetBorder(true) @@ -201,15 +207,15 @@ func (p *Pulse) PulseFailed(err error) { } func (p *Pulse) bindKeys() { - p.actions.Add(ui.KeyActions{ + p.actions.Merge(ui.NewKeyActionsFromMap(ui.KeyMap{ tcell.KeyEnter: ui.NewKeyAction("Goto", p.enterCmd, true), tcell.KeyTab: ui.NewKeyAction("Next", p.nextFocusCmd(1), true), tcell.KeyBacktab: ui.NewKeyAction("Prev", p.nextFocusCmd(-1), true), - }) + })) for i, v := range p.charts { t := cases.Title(language.Und, cases.NoLower).String(client.NewGVR(v.ID()).R()) - p.actions[ui.NumKeys[i]] = ui.NewKeyAction(t, p.sparkFocusCmd(i), true) + p.actions.Add(ui.NumKeys[i], ui.NewKeyAction(t, p.sparkFocusCmd(i), true)) } } @@ -218,7 +224,7 @@ func (p *Pulse) keyboard(evt *tcell.EventKey) *tcell.EventKey { if key == tcell.KeyRune { key = tcell.Key(evt.Rune()) } - if a, ok := p.actions[key]; ok { + if a, ok := p.actions.Get(key); ok { return a.Action(evt) } @@ -283,7 +289,7 @@ func (p *Pulse) GetTable() *Table { } // Actions returns active menu bindings. -func (p *Pulse) Actions() ui.KeyActions { +func (p *Pulse) Actions() *ui.KeyActions { return p.actions } diff --git a/internal/view/pvc.go b/internal/view/pvc.go index ca82465c4d..27d15749de 100644 --- a/internal/view/pvc.go +++ b/internal/view/pvc.go @@ -1,7 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" ) @@ -21,8 +25,8 @@ func NewPersistentVolumeClaim(gvr client.GVR) ResourceViewer { return &v } -func (p *PersistentVolumeClaim) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (p *PersistentVolumeClaim) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyU: ui.NewKeyAction("UsedBy", p.refCmd, true), ui.KeyShiftS: ui.NewKeyAction("Sort Status", p.GetTable().SortColCmd("STATUS", true), false), ui.KeyShiftV: ui.NewKeyAction("Sort Volume", p.GetTable().SortColCmd("VOLUME", true), false), @@ -32,5 +36,5 @@ func (p *PersistentVolumeClaim) bindKeys(aa ui.KeyActions) { } func (p *PersistentVolumeClaim) refCmd(evt *tcell.EventKey) *tcell.EventKey { - return scanRefs(evt, p.App(), p.GetTable(), "v1/persistentvolumeclaims") + return scanRefs(evt, p.App(), p.GetTable(), dao.PvcGVR) } diff --git a/internal/view/pvc_test.go b/internal/view/pvc_test.go index 1559549882..cdaaaaf2bb 100644 --- a/internal/view/pvc_test.go +++ b/internal/view/pvc_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( diff --git a/internal/view/rbac.go b/internal/view/rbac.go index d3b4e8af7c..5583122861 100644 --- a/internal/view/rbac.go +++ b/internal/view/rbac.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -20,20 +23,18 @@ func NewRbac(gvr client.GVR) ResourceViewer { ResourceViewer: NewBrowser(gvr), } r.AddBindKeysFn(r.bindKeys) - r.GetTable().SetSortCol("APIGROUP", true) + r.GetTable().SetSortCol("API-GROUP", true) r.GetTable().SetEnterFn(blankEnterFn) return &r } -func (r *Rbac) bindKeys(aa ui.KeyActions) { +func (r *Rbac) bindKeys(aa *ui.KeyActions) { aa.Delete(ui.KeyShiftA, tcell.KeyCtrlSpace, ui.KeySpace) - aa.Add(ui.KeyActions{ - ui.KeyShiftO: ui.NewKeyAction("Sort APIGroup", r.GetTable().SortColCmd("APIGROUP", true), false), - }) + aa.Add(ui.KeyShiftA, ui.NewKeyAction("Sort API-Group", r.GetTable().SortColCmd("API-GROUP", true), false)) } -func showRules(app *App, _ ui.Tabular, gvr, path string) { +func showRules(app *App, _ ui.Tabular, gvr client.GVR, path string) { v := NewRbac(client.NewGVR("rbac")) v.SetContextFn(rbacCtx(gvr, path)) @@ -42,11 +43,11 @@ func showRules(app *App, _ ui.Tabular, gvr, path string) { } } -func rbacCtx(gvr, path string) ContextFunc { +func rbacCtx(gvr client.GVR, path string) ContextFunc { return func(ctx context.Context) context.Context { ctx = context.WithValue(ctx, internal.KeyPath, path) return context.WithValue(ctx, internal.KeyGVR, gvr) } } -func blankEnterFn(_ *App, _ ui.Tabular, _, _ string) {} +func blankEnterFn(_ *App, _ ui.Tabular, _ client.GVR, _ string) {} diff --git a/internal/view/rbac_test.go b/internal/view/rbac_test.go index 2c0972c81d..5d72d3588d 100644 --- a/internal/view/rbac_test.go +++ b/internal/view/rbac_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( diff --git a/internal/view/reference.go b/internal/view/reference.go index b8ed13bdf1..2f08cc7bba 100644 --- a/internal/view/reference.go +++ b/internal/view/reference.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -30,15 +33,15 @@ func (r *Reference) Init(ctx context.Context) error { if err := r.ResourceViewer.Init(ctx); err != nil { return err } - r.GetTable().GetModel().SetNamespace(client.AllNamespaces) + r.GetTable().GetModel().SetNamespace(client.BlankNamespace) return nil } -func (r *Reference) bindKeys(aa ui.KeyActions) { +func (r *Reference) bindKeys(aa *ui.KeyActions) { aa.Delete(ui.KeyShiftA, tcell.KeyCtrlS, tcell.KeyCtrlSpace, ui.KeySpace) aa.Delete(tcell.KeyCtrlW, tcell.KeyCtrlL, tcell.KeyCtrlZ) - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ tcell.KeyEnter: ui.NewKeyAction("Goto", r.gotoCmd, true), ui.KeyShiftV: ui.NewKeyAction("Sort GVR", r.GetTable().SortColCmd("GVR", true), false), }) diff --git a/internal/view/reference_test.go b/internal/view/reference_test.go index c7e17f3719..0e3578f7f7 100644 --- a/internal/view/reference_test.go +++ b/internal/view/reference_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( diff --git a/internal/view/registrar.go b/internal/view/registrar.go index 1d6b375f47..d199a44988 100644 --- a/internal/view/registrar.go +++ b/internal/view/registrar.go @@ -1,8 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/ui" ) func loadCustomViewers() MetaViewers { @@ -12,7 +14,7 @@ func loadCustomViewers() MetaViewers { appsViewers(m) rbacViewers(m) batchViewers(m) - extViewers(m) + crdViewers(m) helmViewers(m) return m @@ -20,7 +22,7 @@ func loadCustomViewers() MetaViewers { func helmViewers(vv MetaViewers) { vv[client.NewGVR("helm")] = MetaViewer{ - viewerFn: NewHelm, + viewerFn: NewHelmChart, } } @@ -58,16 +60,18 @@ func coreViewers(vv MetaViewers) { } func miscViewers(vv MetaViewers) { + vv[client.NewGVR("workloads")] = MetaViewer{ + viewerFn: NewWorkload, + } vv[client.NewGVR("contexts")] = MetaViewer{ viewerFn: NewContext, } - // BOZO!! revamp with latest... - // vv[client.NewGVR("openfaas")] = MetaViewer{ - // viewerFn: NewOpenFaas, - // } vv[client.NewGVR("containers")] = MetaViewer{ viewerFn: NewContainer, } + vv[client.NewGVR("scans")] = MetaViewer{ + viewerFn: NewImageScan, + } vv[client.NewGVR("portforwards")] = MetaViewer{ viewerFn: NewPortForward, } @@ -86,9 +90,10 @@ func miscViewers(vv MetaViewers) { vv[client.NewGVR("pulses")] = MetaViewer{ viewerFn: NewPulse, } - vv[client.NewGVR("popeye")] = MetaViewer{ - viewerFn: NewPopeye, - } + // !!BOZO!! Popeye + // vv[client.NewGVR("popeye")] = MetaViewer{ + // viewerFn: NewPopeye, + // } vv[client.NewGVR("sanitizer")] = MetaViewer{ viewerFn: NewSanitizer, } @@ -148,13 +153,8 @@ func batchViewers(vv MetaViewers) { } } -func extViewers(vv MetaViewers) { +func crdViewers(vv MetaViewers) { vv[client.NewGVR("apiextensions.k8s.io/v1/customresourcedefinitions")] = MetaViewer{ - enterFn: showCRD, + viewerFn: NewCRD, } } - -func showCRD(app *App, _ ui.Tabular, _, path string) { - _, crd := client.Namespaced(path) - app.gotoResource(crd, "", false) -} diff --git a/internal/view/restart_extender.go b/internal/view/restart_extender.go index 18f25e7ae6..668e9bad71 100644 --- a/internal/view/restart_extender.go +++ b/internal/view/restart_extender.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -26,13 +29,16 @@ func NewRestartExtender(v ResourceViewer) ResourceViewer { } // BindKeys creates additional menu actions. -func (r *RestartExtender) bindKeys(aa ui.KeyActions) { +func (r *RestartExtender) bindKeys(aa *ui.KeyActions) { if r.App().Config.K9s.IsReadOnly() { return } - aa.Add(ui.KeyActions{ - ui.KeyR: ui.NewKeyAction("Restart", r.restartCmd, true), - }) + aa.Add(ui.KeyR, ui.NewKeyActionWithOpts("Restart", r.restartCmd, + ui.ActionOpts{ + Visible: true, + Dangerous: true, + }, + )) } func (r *RestartExtender) restartCmd(evt *tcell.EventKey) *tcell.EventKey { diff --git a/internal/view/rs.go b/internal/view/rs.go index 14fd11ef54..07178563a1 100644 --- a/internal/view/rs.go +++ b/internal/view/rs.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -18,7 +21,11 @@ type ReplicaSet struct { // NewReplicaSet returns a new viewer. func NewReplicaSet(gvr client.GVR) ResourceViewer { r := ReplicaSet{ - ResourceViewer: NewBrowser(gvr), + ResourceViewer: NewOwnerExtender( + NewVulnerabilityExtender( + NewBrowser(gvr), + ), + ), } r.AddBindKeysFn(r.bindKeys) r.GetTable().SetEnterFn(r.showPods) @@ -26,8 +33,8 @@ func NewReplicaSet(gvr client.GVR) ResourceViewer { return &r } -func (r *ReplicaSet) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (r *ReplicaSet) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyShiftD: ui.NewKeyAction("Sort Desired", r.GetTable().SortColCmd("DESIRED", true), false), ui.KeyShiftC: ui.NewKeyAction("Sort Current", r.GetTable().SortColCmd("CURRENT", true), false), ui.KeyShiftR: ui.NewKeyAction("Sort Ready", r.GetTable().SortColCmd(readyCol, true), false), @@ -35,7 +42,7 @@ func (r *ReplicaSet) bindKeys(aa ui.KeyActions) { }) } -func (r *ReplicaSet) showPods(app *App, model ui.Tabular, gvr, path string) { +func (r *ReplicaSet) showPods(app *App, _ ui.Tabular, _ client.GVR, path string) { var drs dao.ReplicaSet rs, err := drs.Load(app.factory, path) if err != nil { diff --git a/internal/view/sa.go b/internal/view/sa.go index 622fccf58d..63145e7cd4 100644 --- a/internal/view/sa.go +++ b/internal/view/sa.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -26,8 +29,8 @@ func NewServiceAccount(gvr client.GVR) ResourceViewer { return &s } -func (s *ServiceAccount) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (s *ServiceAccount) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyU: ui.NewKeyAction("UsedBy", s.refCmd, true), tcell.KeyEnter: ui.NewKeyAction("Rules", s.policyCmd, true), }) @@ -38,7 +41,7 @@ func (s *ServiceAccount) subjectCtx(ctx context.Context) context.Context { } func (s *ServiceAccount) refCmd(evt *tcell.EventKey) *tcell.EventKey { - return scanSARefs(evt, s.App(), s.GetTable(), "v1/serviceaccounts") + return scanSARefs(evt, s.App(), s.GetTable(), dao.SaGVR) } func (s *ServiceAccount) policyCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -53,7 +56,7 @@ func (s *ServiceAccount) policyCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } -func scanSARefs(evt *tcell.EventKey, a *App, t *Table, gvr string) *tcell.EventKey { +func scanSARefs(evt *tcell.EventKey, a *App, t *Table, gvr client.GVR) *tcell.EventKey { path := t.GetSelectedItem() if path == "" { return evt diff --git a/internal/view/sanitizer.go b/internal/view/sanitizer.go index 2a63d47ec2..9ec116d73e 100644 --- a/internal/view/sanitizer.go +++ b/internal/view/sanitizer.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -9,6 +12,7 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/xray" "github.com/derailed/tcell/v2" @@ -21,7 +25,7 @@ import ( var _ ResourceViewer = (*Sanitizer)(nil) -// Sanitizer represents an sanitizer tree view. +// Sanitizer represents a sanitizer tree view. type Sanitizer struct { *ui.Tree @@ -43,6 +47,9 @@ func NewSanitizer(gvr client.GVR) ResourceViewer { } } +func (s *Sanitizer) SetFilter(string) {} +func (s *Sanitizer) SetLabelFilter(map[string]string) {} + // Init initializes the view. func (s *Sanitizer) Init(ctx context.Context) error { s.envFn = s.k9sEnv @@ -93,7 +100,7 @@ func (*Sanitizer) InCmdMode() bool { // ExtraHints returns additional hints. func (s *Sanitizer) ExtraHints() map[string]string { - if s.app.Config.K9s.NoIcons { + if s.app.Config.K9s.UI.NoIcons { return nil } return xray.EmojiInfo() @@ -103,7 +110,7 @@ func (s *Sanitizer) ExtraHints() map[string]string { func (s *Sanitizer) SetInstance(string) {} func (s *Sanitizer) bindKeys() { - s.Actions().Add(ui.KeyActions{ + s.Actions().Bulk(ui.KeyMap{ ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", s.activateCmd, false), tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", s.resetCmd, false), tcell.KeyEnter: ui.NewKeyAction("Goto", s.gotoCmd, true), @@ -202,7 +209,7 @@ func (s *Sanitizer) resetCmd(evt *tcell.EventKey) *tcell.EventKey { func (s *Sanitizer) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { if s.CmdBuff().IsActive() { - if ui.IsLabelSelector(s.CmdBuff().GetText()) { + if internal.IsLabelSelector(s.CmdBuff().GetText()) { s.Start() } s.CmdBuff().SetActive(false) @@ -231,16 +238,16 @@ func (s *Sanitizer) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { func (s *Sanitizer) filter(root *xray.TreeNode) *xray.TreeNode { q := s.CmdBuff().GetText() - if s.CmdBuff().Empty() || ui.IsLabelSelector(q) { + if s.CmdBuff().Empty() || internal.IsLabelSelector(q) { return root } s.UpdateTitle() - if ui.IsFuzzySelector(q) { - return root.Filter(q, fuzzyFilter) + if f, ok := internal.IsFuzzySelector(q); ok { + return root.Filter(f, fuzzyFilter) } - if ui.IsInverseSelector(q) { + if internal.IsInverseSelector(q) { return root.Filter(q, rxInverseFilter) } @@ -263,7 +270,7 @@ func (s *Sanitizer) TreeLoadFailed(err error) { } func (s *Sanitizer) update(node *xray.TreeNode) { - root := makeTreeNode(node, s.ExpandNodes(), s.app.Config.K9s.NoIcons, s.app.Styles) + root := makeTreeNode(node, s.ExpandNodes(), s.app.Config.K9s.UI.NoIcons, s.app.Styles) if node == nil { s.app.QueueUpdateDraw(func() { s.SetRoot(root) @@ -310,7 +317,7 @@ func (s *Sanitizer) TreeChanged(node *xray.TreeNode) { } func (s *Sanitizer) hydrate(parent *tview.TreeNode, n *xray.TreeNode) { - node := makeTreeNode(n, s.ExpandNodes(), s.app.Config.K9s.NoIcons, s.app.Styles) + node := makeTreeNode(n, s.ExpandNodes(), s.app.Config.K9s.UI.NoIcons, s.app.Styles) for _, c := range n.Children { s.hydrate(node, c) } @@ -411,16 +418,16 @@ func (s *Sanitizer) styleTitle() string { var title string if ns == client.ClusterScope { - title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, s.Count), s.app.Styles.Frame()) + title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, render.AsThousands(int64(s.Count))), s.app.Styles.Frame()) } else { - title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, s.Count), s.app.Styles.Frame()) + title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, render.AsThousands(int64(s.Count))), s.app.Styles.Frame()) } buff := s.CmdBuff().GetText() if buff == "" { return title } - if ui.IsLabelSelector(buff) { + if internal.IsLabelSelector(buff) { buff = ui.TrimLabelSelector(buff) } diff --git a/internal/view/scale_extender.go b/internal/view/scale_extender.go index 15754d1c71..e402bd5ae8 100644 --- a/internal/view/scale_extender.go +++ b/internal/view/scale_extender.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -6,6 +9,8 @@ import ( "strconv" "strings" + "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" @@ -26,13 +31,16 @@ func NewScaleExtender(r ResourceViewer) ResourceViewer { return &s } -func (s *ScaleExtender) bindKeys(aa ui.KeyActions) { +func (s *ScaleExtender) bindKeys(aa *ui.KeyActions) { if s.App().Config.K9s.IsReadOnly() { return } - aa.Add(ui.KeyActions{ - ui.KeyS: ui.NewKeyAction("Scale", s.scaleCmd, true), - }) + aa.Add(ui.KeyS, ui.NewKeyActionWithOpts("Scale", s.scaleCmd, + ui.ActionOpts{ + Visible: true, + Dangerous: true, + }, + )) } func (s *ScaleExtender) scaleCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -76,7 +84,8 @@ func (s *ScaleExtender) valueOf(col string) (string, error) { } func (s *ScaleExtender) makeScaleForm(sels []string) (*tview.Form, error) { - f := s.makeStyledForm() + styles := s.App().Styles.Dialog() + f := s.makeStyledForm(styles) factor := "0" if len(sels) == 1 { @@ -119,10 +128,15 @@ func (s *ScaleExtender) makeScaleForm(sels []string) (*tview.Form, error) { s.App().Flash().Infof("%s %s scaled successfully", s.GVR().R(), sels[0]) } }) - f.AddButton("Cancel", func() { s.dismissDialog() }) + for i := 0; i < 2; i++ { + if b := f.GetButton(i); b != nil { + b.SetBackgroundColorActivated(styles.ButtonFocusBgColor.Color()) + b.SetLabelColorActivated(styles.ButtonFocusFgColor.Color()) + } + } return f, nil } @@ -131,14 +145,14 @@ func (s *ScaleExtender) dismissDialog() { s.App().Content.RemovePage(scaleDialogKey) } -func (s *ScaleExtender) makeStyledForm() *tview.Form { +func (s *ScaleExtender) makeStyledForm(styles config.Dialog) *tview.Form { f := tview.NewForm() f.SetItemPadding(0) f.SetButtonsAlign(tview.AlignCenter). - SetButtonBackgroundColor(tview.Styles.PrimitiveBackgroundColor). - SetButtonTextColor(tview.Styles.PrimaryTextColor). - SetLabelColor(tcell.ColorAqua). - SetFieldTextColor(tcell.ColorOrange) + SetButtonBackgroundColor(styles.ButtonBgColor.Color()). + SetButtonTextColor(styles.ButtonBgColor.Color()). + SetLabelColor(styles.LabelFgColor.Color()). + SetFieldTextColor(styles.FieldFgColor.Color()) return f } diff --git a/internal/view/screen_dump.go b/internal/view/screen_dump.go index 5f02fd39a9..83bbca09ac 100644 --- a/internal/view/screen_dump.go +++ b/internal/view/screen_dump.go @@ -1,12 +1,14 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( "context" - "path/filepath" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/data" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" "github.com/rs/zerolog/log" @@ -25,7 +27,7 @@ func NewScreenDump(gvr client.GVR) ResourceViewer { s.GetTable().SetBorderFocusColor(tcell.ColorSteelBlue) s.GetTable().SetSelectedStyle(tcell.StyleDefault.Foreground(tcell.ColorWhite).Background(tcell.ColorRoyalBlue).Attributes(tcell.AttrNone)) s.GetTable().SetSortCol(ageCol, true) - s.GetTable().SelectRow(1, true) + s.GetTable().SelectRow(1, 0, true) s.GetTable().SetEnterFn(s.edit) s.SetContextFn(s.dirContext) @@ -33,8 +35,8 @@ func NewScreenDump(gvr client.GVR) ResourceViewer { } func (s *ScreenDump) dirContext(ctx context.Context) context.Context { - dir := filepath.Join(s.App().Config.K9s.GetScreenDumpDir(), s.App().Config.K9s.CurrentContextDir()) - if err := config.EnsureFullPath(dir, config.DefaultDirMod); err != nil { + dir := s.App().Config.K9s.ContextScreenDumpDir() + if err := data.EnsureFullPath(dir, data.DefaultDirMod); err != nil { s.App().Flash().Err(err) return ctx } @@ -42,7 +44,7 @@ func (s *ScreenDump) dirContext(ctx context.Context) context.Context { return context.WithValue(ctx, internal.KeyDir, dir) } -func (s *ScreenDump) edit(app *App, model ui.Tabular, gvr, path string) { +func (s *ScreenDump) edit(app *App, _ ui.Tabular, _ client.GVR, path string) { log.Debug().Msgf("ScreenDump selection is %q", path) s.Stop() diff --git a/internal/view/screen_dump_test.go b/internal/view/screen_dump_test.go index 0e191f1b54..b812435b43 100644 --- a/internal/view/screen_dump_test.go +++ b/internal/view/screen_dump_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( diff --git a/internal/view/secret.go b/internal/view/secret.go index bec084d0f8..d59e77ee1c 100644 --- a/internal/view/secret.go +++ b/internal/view/secret.go @@ -1,13 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" - v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/yaml" ) @@ -26,15 +28,15 @@ func NewSecret(gvr client.GVR) ResourceViewer { return &s } -func (s *Secret) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ +func (s *Secret) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ ui.KeyX: ui.NewKeyAction("Decode", s.decodeCmd, true), ui.KeyU: ui.NewKeyAction("UsedBy", s.refCmd, true), }) } func (s *Secret) refCmd(evt *tcell.EventKey) *tcell.EventKey { - return scanRefs(evt, s.App(), s.GetTable(), "v1/secrets") + return scanRefs(evt, s.App(), s.GetTable(), dao.SecGVR) } func (s *Secret) decodeCmd(evt *tcell.EventKey) *tcell.EventKey { @@ -49,24 +51,19 @@ func (s *Secret) decodeCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - var secret v1.Secret - err = runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &secret) + d, err := dao.ExtractSecrets(o.(*unstructured.Unstructured)) if err != nil { s.App().Flash().Err(err) return nil } - d := make(map[string]string, len(secret.Data)) - for k, val := range secret.Data { - d[k] = string(val) - } raw, err := yaml.Marshal(d) if err != nil { s.App().Flash().Errf("Error decoding secret %s", err) return nil } - details := NewDetails(s.App(), "Secret Decoder", path, true).Update(string(raw)) + details := NewDetails(s.App(), "Secret Decoder", path, contentYAML, true).Update(string(raw)) if err := s.App().inject(details, false); err != nil { s.App().Flash().Err(err) } diff --git a/internal/view/secret_test.go b/internal/view/secret_test.go index c2d9d4df4f..fc07d6825f 100644 --- a/internal/view/secret_test.go +++ b/internal/view/secret_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( diff --git a/internal/view/sts.go b/internal/view/sts.go index d18f5ad49c..18636d9a2e 100644 --- a/internal/view/sts.go +++ b/internal/view/sts.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -18,10 +21,12 @@ type StatefulSet struct { func NewStatefulSet(gvr client.GVR) ResourceViewer { var s StatefulSet s.ResourceViewer = NewPortForwardExtender( - NewRestartExtender( - NewScaleExtender( - NewImageExtender( - NewLogsExtender(NewBrowser(gvr), s.logOptions), + NewVulnerabilityExtender( + NewRestartExtender( + NewScaleExtender( + NewImageExtender( + NewLogsExtender(NewBrowser(gvr), s.logOptions), + ), ), ), ), @@ -37,52 +42,19 @@ func (s *StatefulSet) logOptions(prev bool) (*dao.LogOptions, error) { if path == "" { return nil, errors.New("you must provide a selection") } - sts, err := s.getInstance(path) if err != nil { return nil, err } - cc := sts.Spec.Template.Spec.Containers - var ( - co, dco string - allCos bool - ) - if c, ok := dao.GetDefaultContainer(sts.Spec.Template.ObjectMeta, sts.Spec.Template.Spec); ok { - co, dco = c, c - } else if len(cc) == 1 { - co = cc[0].Name - } else { - dco, allCos = cc[0].Name, true - } - - cfg := s.App().Config.K9s.Logger - opts := dao.LogOptions{ - Path: path, - Container: co, - Lines: int64(cfg.TailCount), - SingleContainer: len(cc) == 1, - SinceSeconds: cfg.SinceSeconds, - AllContainers: allCos, - ShowTimestamp: cfg.ShowTime, - ShowJSON: cfg.ShowJSON, - Previous: prev, - } - if co == "" { - opts.AllContainers = true - } - opts.DefaultContainer = dco - - return &opts, nil + return podLogOptions(s.App(), path, prev, sts.ObjectMeta, sts.Spec.Template.Spec), nil } -func (s *StatefulSet) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ - ui.KeyShiftR: ui.NewKeyAction("Sort Ready", s.GetTable().SortColCmd(readyCol, true), false), - }) +func (s *StatefulSet) bindKeys(aa *ui.KeyActions) { + aa.Add(ui.KeyShiftR, ui.NewKeyAction("Sort Ready", s.GetTable().SortColCmd(readyCol, true), false)) } -func (s *StatefulSet) showPods(app *App, _ ui.Tabular, _, path string) { +func (s *StatefulSet) showPods(app *App, _ ui.Tabular, _ client.GVR, path string) { i, err := s.getInstance(path) if err != nil { app.Flash().Err(err) @@ -94,5 +66,6 @@ func (s *StatefulSet) showPods(app *App, _ ui.Tabular, _, path string) { func (s *StatefulSet) getInstance(path string) (*appsv1.StatefulSet, error) { var sts dao.StatefulSet + return sts.GetInstance(s.App().factory, path) } diff --git a/internal/view/sts_test.go b/internal/view/sts_test.go index 6b7374906c..5651fd38df 100644 --- a/internal/view/sts_test.go +++ b/internal/view/sts_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( @@ -13,5 +16,5 @@ func TestStatefulSetNew(t *testing.T) { assert.Nil(t, s.Init(makeCtx())) assert.Equal(t, "StatefulSets", s.Name()) - assert.Equal(t, 12, len(s.Hints())) + assert.Equal(t, 13, len(s.Hints())) } diff --git a/internal/view/svc.go b/internal/view/svc.go index 82a18d900b..1517116819 100644 --- a/internal/view/svc.go +++ b/internal/view/svc.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -43,14 +46,14 @@ func NewService(gvr client.GVR) ResourceViewer { // Protocol... -func (s *Service) bindKeys(aa ui.KeyActions) { - aa.Add(ui.KeyActions{ - tcell.KeyCtrlL: ui.NewKeyAction("Bench Run/Stop", s.toggleBenchCmd, true), - ui.KeyShiftT: ui.NewKeyAction("Sort Type", s.GetTable().SortColCmd("TYPE", true), false), +func (s *Service) bindKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ + ui.KeyB: ui.NewKeyAction("Bench Run/Stop", s.toggleBenchCmd, true), + ui.KeyShiftT: ui.NewKeyAction("Sort Type", s.GetTable().SortColCmd("TYPE", true), false), }) } -func (s *Service) showPods(a *App, _ ui.Tabular, gvr, path string) { +func (s *Service) showPods(a *App, _ ui.Tabular, _ client.GVR, path string) { var res dao.Service res.Init(a.factory, s.GVR()) @@ -63,8 +66,12 @@ func (s *Service) showPods(a *App, _ ui.Tabular, gvr, path string) { a.Flash().Warnf("No matching pods. Service %s is an external service.", path) return } + if svc.Spec.Selector == nil { + a.Flash().Warnf("No matching pods. Service %s does not provide any selectors", path) + return + } - showPodsWithLabels(a, path, svc.Spec.Selector) + showPods(a, path, toLabelsStr(svc.Spec.Selector), "") } func (s *Service) checkSvc(svc *v1.Service) error { @@ -146,14 +153,30 @@ func (s *Service) runBenchmark(port string, cfg config.BenchConfig) error { } var err error - base := "http://" + cfg.HTTP.Host + ":" + port + cfg.HTTP.Path + base := cfg.HTTP.Host + if !strings.Contains(base, ":") { + base += ":" + port + cfg.HTTP.Path + } else { + base += cfg.HTTP.Path + } + if strings.Index(base, "http") != 0 { + base = "http://" + base + } + if s.bench, err = perf.NewBenchmark(base, s.App().version, cfg); err != nil { return err } s.App().Status(model.FlashWarn, "Benchmark in progress...") - log.Debug().Msg("Bench starting...") - go s.bench.Run(s.App().Config.K9s.CurrentCluster, s.benchDone) + log.Debug().Msg("Benchmark starting...") + + ct, err := s.App().Config.K9s.ActiveContext() + if err != nil { + return err + } + name := s.App().Config.K9s.ActiveContextName() + + go s.bench.Run(ct.ClusterName, name, s.benchDone) return nil } diff --git a/internal/view/svc_test.go b/internal/view/svc_test.go index fef2e960fa..cc50f92fe7 100644 --- a/internal/view/svc_test.go +++ b/internal/view/svc_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view_test import ( @@ -170,5 +173,5 @@ func TestServiceNew(t *testing.T) { assert.Nil(t, s.Init(makeCtx())) assert.Equal(t, "Services", s.Name()) - assert.Equal(t, 10, len(s.Hints())) + assert.Equal(t, 11, len(s.Hints())) } diff --git a/internal/view/table.go b/internal/view/table.go index ab45e5fe74..d0f60d37a0 100644 --- a/internal/view/table.go +++ b/internal/view/table.go @@ -1,13 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( "context" + "path/filepath" "strings" "time" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tcell/v2" "github.com/rs/zerolog/log" @@ -42,6 +47,13 @@ func (t *Table) Init(ctx context.Context) (err error) { ctx = context.WithValue(ctx, internal.KeyHasMetrics, t.app.Conn().HasMetrics()) } ctx = context.WithValue(ctx, internal.KeyStyles, t.app.Styles) + if !t.app.Config.K9s.UI.Reactive { + if err := t.app.RefreshCustomViews(); err != nil { + log.Warn().Err(err).Msg("CustomViews load failed") + t.app.Logo().Warn("Views load failed!") + } + } + ctx = context.WithValue(ctx, internal.KeyViewConfig, t.app.CustomView) t.Table.Init(ctx) t.SetInputCapture(t.keyboard) @@ -81,7 +93,7 @@ func (t *Table) keyboard(evt *tcell.EventKey) *tcell.EventKey { return evt } - if a, ok := t.Actions()[ui.AsKey(evt)]; ok && !t.app.Content.IsTopDialog() { + if a, ok := t.Actions().Get(ui.AsKey(evt)); ok && !t.app.Content.IsTopDialog() { return a.Action(evt) } @@ -106,11 +118,8 @@ func (t *Table) EnvFn() EnvFunc { func (t *Table) defaultEnv() Env { path := t.GetSelectedItem() - row, ok := t.GetSelectedRow(path) - if !ok { - log.Error().Msgf("unable to locate selected row for %q", path) - } - env := defaultEnv(t.app.Conn().Config(), path, t.GetModel().Peek().Header, row) + row := t.GetSelectedRow(path) + env := defaultEnv(t.app.Conn().Config(), path, t.GetModel().Peek().Header(), row) env["FILTER"] = t.CmdBuff().GetText() if env["FILTER"] == "" { env["NAMESPACE"], env["FILTER"] = client.Namespaced(path) @@ -167,17 +176,17 @@ func (t *Table) BufferActive(state bool, k model.BufferKind) { } func (t *Table) saveCmd(evt *tcell.EventKey) *tcell.EventKey { - if path, err := saveTable(t.app.Config.K9s.GetScreenDumpDir(), t.app.Config.K9s.CurrentContextDir(), t.GVR().R(), t.Path, t.GetFilteredData()); err != nil { + if path, err := saveTable(t.app.Config.K9s.ContextScreenDumpDir(), t.GVR().R(), t.Path, t.GetFilteredData()); err != nil { t.app.Flash().Err(err) } else { - t.app.Flash().Infof("File %s saved successfully!", path) + t.app.Flash().Infof("File saved successfully: %q", render.Truncate(filepath.Base(path), 50)) } return nil } func (t *Table) bindKeys() { - t.Actions().Add(ui.KeyActions{ + t.Actions().Bulk(ui.KeyMap{ ui.KeyHelp: ui.NewKeyAction("Help", t.App().helpCmd, true), ui.KeySpace: ui.NewSharedKeyAction("Mark", t.markCmd, false), tcell.KeyCtrlSpace: ui.NewSharedKeyAction("Mark Range", t.markSpanCmd, false), @@ -211,7 +220,22 @@ func (t *Table) cpCmd(evt *tcell.EventKey) *tcell.EventKey { t.app.Flash().Err(err) return nil } - t.app.Flash().Info("Current selection copied to clipboard...") + t.app.Flash().Info("Resource name copied to clipboard...") + + return nil +} + +func (t *Table) cpNsCmd(evt *tcell.EventKey) *tcell.EventKey { + path := t.GetSelectedItem() + if path == "" { + return evt + } + ns, _ := client.Namespaced(path) + if err := clipboardWrite(ns); err != nil { + t.app.Flash().Err(err) + return nil + } + t.app.Flash().Info("Resource namespace copied to clipboard...") return nil } diff --git a/internal/view/table_helper.go b/internal/view/table_helper.go index e72e0cfd71..34dfc81e0f 100644 --- a/internal/view/table_helper.go +++ b/internal/view/table_helper.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -9,21 +12,21 @@ import ( "time" "github.com/derailed/k9s/internal/client" - "github.com/derailed/k9s/internal/config" - "github.com/derailed/k9s/internal/render" + "github.com/derailed/k9s/internal/config/data" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/ui" "github.com/rs/zerolog/log" ) -func computeFilename(screenDumpDir, context, ns, title, path string) (string, error) { +func computeFilename(dumpPath, ns, title, path string) (string, error) { now := time.Now().UnixNano() - dir := filepath.Join(screenDumpDir, context) + dir := filepath.Join(dumpPath) if err := ensureDir(dir); err != nil { return "", err } - name := title + "-" + config.SanitizeFilename(path) + name := title + "-" + data.SanitizeFileName(path) if path == "" { name = title } @@ -38,13 +41,13 @@ func computeFilename(screenDumpDir, context, ns, title, path string) (string, er return strings.ToLower(filepath.Join(dir, fName)), nil } -func saveTable(screenDumpDir, context, title, path string, data *render.TableData) (string, error) { - ns := data.Namespace +func saveTable(dir, title, path string, data *model1.TableData) (string, error) { + ns := data.GetNamespace() if client.IsClusterWide(ns) { ns = client.NamespaceAll } - fPath, err := computeFilename(screenDumpDir, context, ns, title, path) + fPath, err := computeFilename(dir, ns, title, path) if err != nil { return "", err } @@ -62,15 +65,12 @@ func saveTable(screenDumpDir, context, title, path string, data *render.TableDat }() w := csv.NewWriter(out) - if err := w.Write(data.Header.Columns(true)); err != nil { - return "", err - } + _ = w.Write(data.ColumnNames(true)) - for _, re := range data.RowEvents { - if err := w.Write(re.Row.Fields); err != nil { - return "", err - } - } + data.RowsRange(func(_ int, re model1.RowEvent) bool { + _ = w.Write(re.Row.Fields) + return true + }) w.Flush() if err := w.Error(); err != nil { return "", err diff --git a/internal/view/table_int_test.go b/internal/view/table_int_test.go index cebdc3f5f0..38231eaee4 100644 --- a/internal/view/table_int_test.go +++ b/internal/view/table_int_test.go @@ -1,22 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( "context" + "errors" + "io/fs" "os" - "path/filepath" "testing" "time" "github.com/derailed/k9s/internal" "github.com/derailed/k9s/internal/client" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/mock" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/model1" "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/tview" "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) @@ -26,7 +31,8 @@ func TestTableSave(t *testing.T) { assert.NoError(t, v.Init(makeContext())) v.SetTitle("k9s-test") - dir := filepath.Join(v.app.Config.K9s.GetScreenDumpDir(), v.app.Config.K9s.CurrentCluster) + assert.NoError(t, ensureDumpDir("/tmp/test-dumps")) + dir := v.app.Config.K9s.ContextScreenDumpDir() c1, _ := os.ReadDir(dir) v.saveCmd(nil) @@ -38,28 +44,30 @@ func TestTableNew(t *testing.T) { v := NewTable(client.NewGVR("test")) assert.NoError(t, v.Init(makeContext())) - data := render.NewTableData() - data.Header = render.Header{ - render.HeaderColumn{Name: "NAMESPACE"}, - render.HeaderColumn{Name: "NAME", Align: tview.AlignRight}, - render.HeaderColumn{Name: "FRED"}, - render.HeaderColumn{Name: "AGE", Time: true, Decorator: render.AgeDecorator}, - } - data.RowEvents = render.RowEvents{ - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"ns1", "a", "10", "3m"}, - }, + data := model1.NewTableDataWithRows( + client.NewGVR("test"), + model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "FRED"}, + model1.HeaderColumn{Name: "AGE", Time: true, Decorator: render.AgeDecorator}, }, - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"ns1", "b", "15", "1m"}, + model1.NewRowEventsWithEvts( + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"ns1", "a", "10", "3m"}, + }, }, - }, - } - data.Namespace = "" + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"ns1", "b", "15", "1m"}, + }, + }, + ), + ) + cdata := v.Update(data, false) + v.UpdateUI(cdata, data) - v.Update(data, false) assert.Equal(t, 3, v.GetRowCount()) } @@ -68,6 +76,7 @@ func TestTableViewFilter(t *testing.T) { assert.NoError(t, v.Init(makeContext())) v.SetModel(&mockTableModel{}) v.Refresh() + v.CmdBuff().SetActive(true) v.CmdBuff().SetText("blee", "") @@ -77,7 +86,7 @@ func TestTableViewFilter(t *testing.T) { func TestTableViewSort(t *testing.T) { v := NewTable(client.NewGVR("test")) assert.NoError(t, v.Init(makeContext())) - v.SetModel(&mockTableModel{}) + v.SetModel(new(mockTableModel)) uu := map[string]struct { sortCol string @@ -125,10 +134,11 @@ var _ ui.Tabular = (*mockTableModel)(nil) func (t *mockTableModel) SetInstance(string) {} func (t *mockTableModel) SetLabelFilter(string) {} +func (t *mockTableModel) GetLabelFilter() string { return "" } func (t *mockTableModel) Empty() bool { return false } -func (t *mockTableModel) Count() int { return 1 } +func (t *mockTableModel) RowCount() int { return 1 } func (t *mockTableModel) HasMetrics() bool { return true } -func (t *mockTableModel) Peek() *render.TableData { return makeTableData() } +func (t *mockTableModel) Peek() *model1.TableData { return makeTableData() } func (t *mockTableModel) Refresh(context.Context) error { return nil } func (t *mockTableModel) ClusterWide() bool { return false } func (t *mockTableModel) GetNamespace() string { return "blee" } @@ -156,65 +166,54 @@ func (t *mockTableModel) ToYAML(ctx context.Context, path string) (string, error func (t *mockTableModel) InNamespace(string) bool { return true } func (t *mockTableModel) SetRefreshRate(time.Duration) {} -func makeTableData() *render.TableData { - t := render.NewTableData() - t.Header = render.Header{ - render.HeaderColumn{Name: "NAMESPACE"}, - render.HeaderColumn{Name: "NAME", Align: tview.AlignRight}, - render.HeaderColumn{Name: "FRED"}, - render.HeaderColumn{Name: "AGE", Time: true}, - } - t.RowEvents = render.RowEvents{ - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"ns1", "r3", "10", "3y125d"}, - }, +func makeTableData() *model1.TableData { + return model1.NewTableDataWithRows( + client.NewGVR("test"), + model1.Header{ + model1.HeaderColumn{Name: "NAMESPACE"}, + model1.HeaderColumn{Name: "NAME", Align: tview.AlignRight}, + model1.HeaderColumn{Name: "FRED"}, + model1.HeaderColumn{Name: "AGE", Time: true}, }, - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"ns1", "r2", "15", "2y12d"}, + model1.NewRowEventsWithEvts( + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"ns1", "r3", "10", "3y125d"}, + }, }, - Deltas: render.DeltaRow{"", "", "20", ""}, - }, - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"ns1", "r1", "20", "19h"}, + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"ns1", "r2", "15", "2y12d"}, + }, + Deltas: model1.DeltaRow{"", "", "20", ""}, }, - }, - render.RowEvent{ - Row: render.Row{ - Fields: render.Fields{"ns1", "r0", "15", "10s"}, + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"ns1", "r1", "20", "19h"}, + }, }, - }, - } - - return t + model1.RowEvent{ + Row: model1.Row{ + Fields: model1.Fields{"ns1", "r0", "15", "10s"}, + }, + }, + ), + ) } func makeContext() context.Context { - a := NewApp(config.NewConfig(ks{})) + a := NewApp(mock.NewMockConfig()) ctx := context.WithValue(context.Background(), internal.KeyApp, a) return context.WithValue(ctx, internal.KeyStyles, a.Styles) } -type ks struct{} - -func (k ks) CurrentContextName() (string, error) { - return "test", nil -} - -func (k ks) CurrentClusterName() (string, error) { - return "test", nil -} - -func (k ks) CurrentNamespaceName() (string, error) { - return "test", nil -} - -func (k ks) ClusterNames() (map[string]struct{}, error) { - return map[string]struct{}{"test": {}}, nil -} - -func (k ks) NamespaceNames(nn []v1.Namespace) []string { - return []string{"test"} +func ensureDumpDir(n string) error { + config.AppDumpsDir = n + if _, err := os.Stat(n); errors.Is(err, fs.ErrNotExist) { + return os.Mkdir(n, 0700) + } + if err := os.RemoveAll(n); err != nil { + return err + } + return os.Mkdir(n, 0700) } diff --git a/internal/view/types.go b/internal/view/types.go index c54372de75..76a4c9a624 100644 --- a/internal/view/types.go +++ b/internal/view/types.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -28,7 +31,7 @@ type ( BoostActionsFunc func(ui.KeyActions) // EnterFunc represents an enter key action. - EnterFunc func(app *App, model ui.Tabular, gvr, path string) + EnterFunc func(app *App, model ui.Tabular, gvr client.GVR, path string) // LogOptionsFunc returns the active log options. LogOptionsFunc func(bool) (*dao.LogOptions, error) @@ -37,7 +40,7 @@ type ( ContextFunc func(context.Context) context.Context // BindKeysFunc adds new menu actions. - BindKeysFunc func(ui.KeyActions) + BindKeysFunc func(*ui.KeyActions) ) // ActionExtender enhances a given viewer by adding new menu actions. @@ -57,7 +60,7 @@ type Viewer interface { model.Component // Actions returns active menu bindings. - Actions() ui.KeyActions + Actions() *ui.KeyActions // App returns an app handle. App() *App @@ -70,7 +73,7 @@ type Viewer interface { type TableViewer interface { Viewer - // Table returns a table component. + // GetTable returns a table component. GetTable() *Table } @@ -87,7 +90,7 @@ type ResourceViewer interface { // SetContextFn provision a custom context. SetContextFn(ContextFunc) - // AddBindKeys provision additional key bindings. + // AddBindKeysFn provision additional key bindings. AddBindKeysFn(BindKeysFunc) // SetInstance sets a parent FQN diff --git a/internal/view/user.go b/internal/view/user.go index ab7d9bea50..94f55fd8ec 100644 --- a/internal/view/user.go +++ b/internal/view/user.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -24,9 +27,9 @@ func NewUser(gvr client.GVR) ResourceViewer { return &u } -func (u *User) bindKeys(aa ui.KeyActions) { +func (u *User) bindKeys(aa *ui.KeyActions) { aa.Delete(ui.KeyShiftA, ui.KeyShiftP, tcell.KeyCtrlSpace, ui.KeySpace, tcell.KeyCtrlD, ui.KeyE) - aa.Add(ui.KeyActions{ + aa.Bulk(ui.KeyMap{ tcell.KeyEnter: ui.NewKeyAction("Rules", u.policyCmd, true), ui.KeyShiftK: ui.NewKeyAction("Sort Kind", u.GetTable().SortColCmd("KIND", true), false), }) diff --git a/internal/view/value_extender.go b/internal/view/value_extender.go new file mode 100644 index 0000000000..8d3a244c00 --- /dev/null +++ b/internal/view/value_extender.go @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package view + +import ( + "context" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tcell/v2" + "github.com/rs/zerolog/log" +) + +// ValueExtender adds values actions to a given viewer. +type ValueExtender struct { + ResourceViewer +} + +// NewValueExtender returns a new extender. +func NewValueExtender(r ResourceViewer) ResourceViewer { + p := ValueExtender{ResourceViewer: r} + p.AddBindKeysFn(p.bindKeys) + p.GetTable().SetEnterFn(func(app *App, model ui.Tabular, gvr client.GVR, path string) { + p.valuesCmd(nil) + }) + + return &p +} + +func (v *ValueExtender) bindKeys(aa *ui.KeyActions) { + aa.Add(ui.KeyV, ui.NewKeyAction("Values", v.valuesCmd, true)) +} + +func (v *ValueExtender) valuesCmd(evt *tcell.EventKey) *tcell.EventKey { + path := v.GetTable().GetSelectedItem() + if path == "" { + return evt + } + + showValues(v.defaultCtx(), v.App(), path, v.GVR()) + return nil +} + +func (v *ValueExtender) defaultCtx() context.Context { + return context.WithValue(context.Background(), internal.KeyFactory, v.App().factory) +} + +func showValues(ctx context.Context, app *App, path string, gvr client.GVR) { + vm := model.NewValues(gvr, path) + if err := vm.Init(app.factory); err != nil { + app.Flash().Errf("Initializing the values model failed: %s", err) + } + + toggleValuesCmd := func(evt *tcell.EventKey) *tcell.EventKey { + if err := vm.ToggleValues(); err != nil { + app.Flash().Errf("Values toggle failed: %s", err) + return nil + } + + if err := vm.Refresh(ctx); err != nil { + log.Error().Err(err).Msgf("values refresh failed") + return nil + } + + app.Flash().Infof("Values toggled") + return nil + } + + v := NewLiveView(app, "Values", vm) + v.actions.Add(ui.KeyV, ui.NewKeyAction("Toggle All Values", toggleValuesCmd, true)) + if err := v.app.inject(v, false); err != nil { + v.app.Flash().Err(err) + } +} diff --git a/internal/view/vul_extender.go b/internal/view/vul_extender.go new file mode 100644 index 0000000000..ebc373b6d6 --- /dev/null +++ b/internal/view/vul_extender.go @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package view + +import ( + "context" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/tcell/v2" +) + +// VulnerabilityExtender adds vul image scan extensions. +type VulnerabilityExtender struct { + ResourceViewer +} + +// NewVulnerabilityExtender returns a new extender. +func NewVulnerabilityExtender(r ResourceViewer) ResourceViewer { + v := VulnerabilityExtender{ResourceViewer: r} + v.AddBindKeysFn(v.bindKeys) + + return &v +} + +func (v *VulnerabilityExtender) bindKeys(aa *ui.KeyActions) { + if v.App().Config.K9s.ImageScans.Enable { + aa.Bulk(ui.KeyMap{ + ui.KeyV: ui.NewKeyAction("Show Vulnerabilities", v.showVulCmd, true), + ui.KeyShiftV: ui.NewKeyAction("Sort Vulnerabilities", v.GetTable().SortColCmd("VS", true), false), + }) + } +} + +func (v *VulnerabilityExtender) showVulCmd(evt *tcell.EventKey) *tcell.EventKey { + isv := NewImageScan(client.NewGVR("scans")) + isv.SetContextFn(v.selContext) + if err := v.App().inject(isv, false); err != nil { + v.App().Flash().Err(err) + } + + return nil +} + +func (v *VulnerabilityExtender) selContext(ctx context.Context) context.Context { + ctx = context.WithValue(ctx, internal.KeyPath, v.GetTable().GetSelectedItem()) + return context.WithValue(ctx, internal.KeyGVR, v.GVR()) +} diff --git a/internal/view/workload.go b/internal/view/workload.go new file mode 100644 index 0000000000..4f0ba02fcd --- /dev/null +++ b/internal/view/workload.go @@ -0,0 +1,206 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package view + +import ( + "context" + "fmt" + "strings" + + "github.com/derailed/k9s/internal" + "github.com/derailed/k9s/internal/client" + "github.com/derailed/k9s/internal/dao" + "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/ui" + "github.com/derailed/k9s/internal/ui/dialog" + "github.com/derailed/tcell/v2" + "github.com/rs/zerolog/log" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Workload presents a workload viewer. +type Workload struct { + ResourceViewer +} + +// NewWorkload returns a new viewer. +func NewWorkload(gvr client.GVR) ResourceViewer { + w := Workload{ + ResourceViewer: NewBrowser(gvr), + } + w.GetTable().SetEnterFn(w.showRes) + w.AddBindKeysFn(w.bindKeys) + w.GetTable().SetSortCol("KIND", true) + + return &w +} + +func (w *Workload) bindDangerousKeys(aa *ui.KeyActions) { + aa.Bulk(ui.KeyMap{ + ui.KeyE: ui.NewKeyActionWithOpts("Edit", w.editCmd, + ui.ActionOpts{ + Visible: true, + Dangerous: true, + }), + tcell.KeyCtrlD: ui.NewKeyActionWithOpts("Delete", w.deleteCmd, + ui.ActionOpts{ + Visible: true, + Dangerous: true, + }), + }) +} + +func (w *Workload) bindKeys(aa *ui.KeyActions) { + if !w.App().Config.K9s.IsReadOnly() { + w.bindDangerousKeys(aa) + } + + aa.Bulk(ui.KeyMap{ + ui.KeyShiftK: ui.NewKeyAction("Sort Kind", w.GetTable().SortColCmd("KIND", true), false), + ui.KeyShiftS: ui.NewKeyAction("Sort Status", w.GetTable().SortColCmd(statusCol, true), false), + ui.KeyShiftR: ui.NewKeyAction("Sort Ready", w.GetTable().SortColCmd("READY", true), false), + ui.KeyShiftA: ui.NewKeyAction("Sort Age", w.GetTable().SortColCmd(ageCol, true), false), + ui.KeyY: ui.NewKeyAction(yamlAction, w.yamlCmd, true), + ui.KeyD: ui.NewKeyAction("Describe", w.describeCmd, true), + }) +} + +func parsePath(path string) (client.GVR, string, bool) { + tt := strings.Split(path, "|") + if len(tt) != 3 { + log.Error().Msgf("unable to parse path: %q", path) + return client.NewGVR(""), client.FQN("", ""), false + } + + return client.NewGVR(tt[0]), client.FQN(tt[1], tt[2]), true +} + +func (w *Workload) showRes(app *App, _ ui.Tabular, _ client.GVR, path string) { + gvr, fqn, ok := parsePath(path) + if !ok { + app.Flash().Err(fmt.Errorf("Unable to parse path: %q", path)) + return + } + app.gotoResource(gvr.R(), fqn, false) +} + +func (w *Workload) deleteCmd(evt *tcell.EventKey) *tcell.EventKey { + selections := w.GetTable().GetSelectedItems() + if len(selections) == 0 { + return evt + } + + w.Stop() + defer w.Start() + { + msg := fmt.Sprintf("Delete %s %s?", w.GVR().R(), selections[0]) + if len(selections) > 1 { + msg = fmt.Sprintf("Delete %d marked %s?", len(selections), w.GVR()) + } + w.resourceDelete(selections, msg) + } + + return nil +} + +func (w *Workload) defaultContext(gvr client.GVR, fqn string) context.Context { + ctx := context.WithValue(context.Background(), internal.KeyFactory, w.App().factory) + ctx = context.WithValue(ctx, internal.KeyGVR, gvr) + if fqn != "" { + ctx = context.WithValue(ctx, internal.KeyPath, fqn) + } + if internal.IsLabelSelector(w.GetTable().CmdBuff().GetText()) { + ctx = context.WithValue(ctx, internal.KeyLabels, ui.TrimLabelSelector(w.GetTable().CmdBuff().GetText())) + } + ctx = context.WithValue(ctx, internal.KeyNamespace, client.CleanseNamespace(w.App().Config.ActiveNamespace())) + ctx = context.WithValue(ctx, internal.KeyWithMetrics, w.App().factory.Client().HasMetrics()) + + return ctx +} + +func (w *Workload) resourceDelete(selections []string, msg string) { + okFn := func(propagation *metav1.DeletionPropagation, force bool) { + w.GetTable().ShowDeleted() + if len(selections) > 1 { + w.App().Flash().Infof("Delete %d marked %s", len(selections), w.GVR()) + } else { + w.App().Flash().Infof("Delete resource %s %s", w.GVR(), selections[0]) + } + for _, sel := range selections { + gvr, fqn, ok := parsePath(sel) + if !ok { + w.App().Flash().Err(fmt.Errorf("Unable to parse path: %q", sel)) + return + } + + grace := dao.DefaultGrace + if force { + grace = dao.ForceGrace + } + if err := w.GetTable().GetModel().Delete(w.defaultContext(gvr, fqn), fqn, propagation, grace); err != nil { + w.App().Flash().Errf("Delete failed with `%s", err) + } else { + w.App().factory.DeleteForwarder(sel) + } + w.GetTable().DeleteMark(sel) + } + w.GetTable().Start() + } + dialog.ShowDelete(w.App().Styles.Dialog(), w.App().Content.Pages, msg, okFn, func() {}) +} + +func (w *Workload) describeCmd(evt *tcell.EventKey) *tcell.EventKey { + path := w.GetTable().GetSelectedItem() + if path == "" { + return evt + } + gvr, fqn, ok := parsePath(path) + if !ok { + w.App().Flash().Err(fmt.Errorf("Unable to parse path: %q", path)) + return evt + } + + describeResource(w.App(), nil, gvr, fqn) + + return nil +} + +func (w *Workload) editCmd(evt *tcell.EventKey) *tcell.EventKey { + path := w.GetTable().GetSelectedItem() + if path == "" { + return evt + } + gvr, fqn, ok := parsePath(path) + if !ok { + w.App().Flash().Err(fmt.Errorf("Unable to parse path: %q", path)) + return evt + } + + w.Stop() + defer w.Start() + if err := editRes(w.App(), gvr, fqn); err != nil { + w.App().Flash().Err(err) + } + + return nil +} + +func (w *Workload) yamlCmd(evt *tcell.EventKey) *tcell.EventKey { + path := w.GetTable().GetSelectedItem() + if path == "" { + return evt + } + gvr, fqn, ok := parsePath(path) + if !ok { + w.App().Flash().Err(fmt.Errorf("Unable to parse path: %q", path)) + return evt + } + + v := NewLiveView(w.App(), yamlAction, model.NewYAML(gvr, fqn)) + if err := v.app.inject(v, false); err != nil { + v.app.Flash().Err(err) + } + + return nil +} diff --git a/internal/view/xray.go b/internal/view/xray.go index 74fe8f8c54..34e9e0ccad 100644 --- a/internal/view/xray.go +++ b/internal/view/xray.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -12,6 +15,7 @@ import ( "github.com/derailed/k9s/internal/config" "github.com/derailed/k9s/internal/dao" "github.com/derailed/k9s/internal/model" + "github.com/derailed/k9s/internal/render" "github.com/derailed/k9s/internal/ui" "github.com/derailed/k9s/internal/ui/dialog" "github.com/derailed/k9s/internal/xray" @@ -49,6 +53,9 @@ func NewXray(gvr client.GVR) ResourceViewer { } } +func (x *Xray) SetFilter(string) {} +func (x *Xray) SetLabelFilter(map[string]string) {} + // Init initializes the view. func (x *Xray) Init(ctx context.Context) error { x.envFn = x.k9sEnv @@ -100,7 +107,7 @@ func (*Xray) InCmdMode() bool { // ExtraHints returns additional hints. func (x *Xray) ExtraHints() map[string]string { - if x.app.Config.K9s.NoIcons { + if x.app.Config.K9s.UI.NoIcons { return nil } return xray.EmojiInfo() @@ -110,7 +117,7 @@ func (x *Xray) ExtraHints() map[string]string { func (x *Xray) SetInstance(string) {} func (x *Xray) bindKeys() { - x.Actions().Add(ui.KeyActions{ + x.Actions().Bulk(ui.KeyMap{ ui.KeySlash: ui.NewSharedKeyAction("Filter Mode", x.activateCmd, false), tcell.KeyEscape: ui.NewSharedKeyAction("Filter Reset", x.resetCmd, false), tcell.KeyEnter: ui.NewKeyAction("Goto", x.gotoCmd, true), @@ -123,13 +130,17 @@ func (x *Xray) keyEntered() { } func (x *Xray) refreshActions() { - aa := make(ui.KeyActions) + aa := ui.NewKeyActions() defer func() { - pluginActions(x, aa) - hotKeyActions(x, aa) + if err := pluginActions(x, aa); err != nil { + log.Warn().Err(err).Msg("Plugins load failed") + } + if err := hotKeyActions(x, aa); err != nil { + log.Warn().Err(err).Msg("HotKeys load failed") + } - x.Actions().Add(aa) + x.Actions().Merge(aa) x.app.Menu().HydrateMenu(x.Hints()) }() @@ -151,14 +162,16 @@ func (x *Xray) refreshActions() { } if client.Can(x.meta.Verbs, "edit") { - aa[ui.KeyE] = ui.NewKeyAction("Edit", x.editCmd, true) + aa.Add(ui.KeyE, ui.NewKeyAction("Edit", x.editCmd, true)) } if client.Can(x.meta.Verbs, "delete") { - aa[tcell.KeyCtrlD] = ui.NewKeyAction("Delete", x.deleteCmd, true) + aa.Add(tcell.KeyCtrlD, ui.NewKeyAction("Delete", x.deleteCmd, true)) } if !dao.IsK9sMeta(x.meta) { - aa[ui.KeyY] = ui.NewKeyAction("YAML", x.viewCmd, true) - aa[ui.KeyD] = ui.NewKeyAction("Describe", x.describeCmd, true) + aa.Bulk(ui.KeyMap{ + ui.KeyY: ui.NewKeyAction(yamlAction, x.viewCmd, true), + ui.KeyD: ui.NewKeyAction("Describe", x.describeCmd, true), + }) } switch gvr { @@ -166,16 +179,20 @@ func (x *Xray) refreshActions() { x.Actions().Delete(tcell.KeyEnter) case "containers": x.Actions().Delete(tcell.KeyEnter) - aa[ui.KeyS] = ui.NewKeyAction("Shell", x.shellCmd, true) - aa[ui.KeyL] = ui.NewKeyAction("Logs", x.logsCmd(false), true) - aa[ui.KeyP] = ui.NewKeyAction("Logs Previous", x.logsCmd(true), true) + aa.Bulk(ui.KeyMap{ + ui.KeyS: ui.NewKeyAction("Shell", x.shellCmd, true), + ui.KeyL: ui.NewKeyAction("Logs", x.logsCmd(false), true), + ui.KeyP: ui.NewKeyAction("Logs Previous", x.logsCmd(true), true), + }) case "v1/pods": - aa[ui.KeyS] = ui.NewKeyAction("Shell", x.shellCmd, true) - aa[ui.KeyA] = ui.NewKeyAction("Attach", x.attachCmd, true) - aa[ui.KeyL] = ui.NewKeyAction("Logs", x.logsCmd(false), true) - aa[ui.KeyP] = ui.NewKeyAction("Logs Previous", x.logsCmd(true), true) + aa.Bulk(ui.KeyMap{ + ui.KeyS: ui.NewKeyAction("Shell", x.shellCmd, true), + ui.KeyA: ui.NewKeyAction("Attach", x.attachCmd, true), + ui.KeyL: ui.NewKeyAction("Logs", x.logsCmd(false), true), + ui.KeyP: ui.NewKeyAction("Logs Previous", x.logsCmd(true), true), + }) } - x.Actions().Add(aa) + x.Actions().Merge(aa) } // GetSelectedPath returns the current selection as string. @@ -236,8 +253,8 @@ func (x *Xray) k9sEnv() Env { } // Aliases returns all available aliases. -func (x *Xray) Aliases() []string { - return append(x.meta.ShortNames, x.meta.SingularName, x.meta.Name) +func (x *Xray) Aliases() map[string]struct{} { + return aliasesFor(x.meta, x.app.command.AliasesFor(x.meta.Name)) } func (x *Xray) logsCmd(prev bool) func(evt *tcell.EventKey) *tcell.EventKey { @@ -262,7 +279,7 @@ func (x *Xray) showLogs(spec *xray.NodeSpec, prev bool) { } ns, _ := client.Namespaced(path) - _, err := x.app.factory.CanForResource(ns, "v1/pods", client.MonitorAccess) + _, err := x.app.factory.CanForResource(ns, "v1/pods", client.ListAccess) if err != nil { x.app.Flash().Err(err) return @@ -338,7 +355,7 @@ func (x *Xray) viewCmd(evt *tcell.EventKey) *tcell.EventKey { return nil } - details := NewDetails(x.app, "YAML", spec.Path(), true).Update(raw) + details := NewDetails(x.app, yamlAction, spec.Path(), contentYAML, true).Update(raw) if err := x.app.inject(details, false); err != nil { x.app.Flash().Err(err) } @@ -388,7 +405,7 @@ func (x *Xray) describe(gvr, path string) { return } - details := NewDetails(x.app, "Describe", path, true).Update(yaml) + details := NewDetails(x.app, "Describe", path, contentYAML, true).Update(yaml) if err := x.app.inject(details, false); err != nil { x.app.Flash().Err(err) } @@ -408,7 +425,7 @@ func (x *Xray) editCmd(evt *tcell.EventKey) *tcell.EventKey { args = append(args, "edit") args = append(args, client.NewGVR(spec.GVR()).R()) args = append(args, "-n", ns) - args = append(args, "--context", x.app.Config.K9s.CurrentContext) + args = append(args, "--context", x.app.Config.K9s.ActiveContextName()) if cfg := x.app.Conn().Config().Flags().KubeConfig; cfg != nil && *cfg != "" { args = append(args, "--kubeconfig", *cfg) } @@ -443,7 +460,7 @@ func (x *Xray) resetCmd(evt *tcell.EventKey) *tcell.EventKey { func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { if x.CmdBuff().IsActive() { - if ui.IsLabelSelector(x.CmdBuff().GetText()) { + if internal.IsLabelSelector(x.CmdBuff().GetText()) { x.Start() } x.CmdBuff().SetActive(false) @@ -466,16 +483,16 @@ func (x *Xray) gotoCmd(evt *tcell.EventKey) *tcell.EventKey { func (x *Xray) filter(root *xray.TreeNode) *xray.TreeNode { q := x.CmdBuff().GetText() - if x.CmdBuff().Empty() || ui.IsLabelSelector(q) { + if x.CmdBuff().Empty() || internal.IsLabelSelector(q) { return root } x.UpdateTitle() - if ui.IsFuzzySelector(q) { - return root.Filter(q, fuzzyFilter) + if f, ok := internal.IsFuzzySelector(q); ok { + return root.Filter(f, fuzzyFilter) } - if ui.IsInverseSelector(q) { + if internal.IsInverseSelector(q) { return root.Filter(q, rxInverseFilter) } @@ -498,7 +515,7 @@ func (x *Xray) TreeLoadFailed(err error) { } func (x *Xray) update(node *xray.TreeNode) { - root := makeTreeNode(node, x.ExpandNodes(), x.app.Config.K9s.NoIcons, x.app.Styles) + root := makeTreeNode(node, x.ExpandNodes(), x.app.Config.K9s.UI.NoIcons, x.app.Styles) if node == nil { x.app.QueueUpdateDraw(func() { x.SetRoot(root) @@ -545,7 +562,7 @@ func (x *Xray) TreeChanged(node *xray.TreeNode) { } func (x *Xray) hydrate(parent *tview.TreeNode, n *xray.TreeNode) { - node := makeTreeNode(n, x.ExpandNodes(), x.app.Config.K9s.NoIcons, x.app.Styles) + node := makeTreeNode(n, x.ExpandNodes(), x.app.Config.K9s.UI.NoIcons, x.app.Styles) for _, c := range n.Children { x.hydrate(node, c) } @@ -641,16 +658,16 @@ func (x *Xray) styleTitle() string { var title string if ns == client.ClusterScope { - title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, x.Count), x.app.Styles.Frame()) + title = ui.SkinTitle(fmt.Sprintf(ui.TitleFmt, base, render.AsThousands(int64(x.Count))), x.app.Styles.Frame()) } else { - title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, x.Count), x.app.Styles.Frame()) + title = ui.SkinTitle(fmt.Sprintf(ui.NSTitleFmt, base, ns, render.AsThousands(int64(x.Count))), x.app.Styles.Frame()) } buff := x.CmdBuff().GetText() if buff == "" { return title } - if ui.IsLabelSelector(buff) { + if internal.IsLabelSelector(buff) { buff = ui.TrimLabelSelector(buff) } diff --git a/internal/view/yaml.go b/internal/view/yaml.go index d95d7dc79b..828e96e3f0 100644 --- a/internal/view/yaml.go +++ b/internal/view/yaml.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( @@ -9,6 +12,7 @@ import ( "time" "github.com/derailed/k9s/internal/config" + "github.com/derailed/k9s/internal/config/data" "github.com/derailed/tview" "github.com/rs/zerolog/log" ) @@ -26,7 +30,6 @@ const ( func colorizeYAML(style config.Yaml, raw string) string { lines := strings.Split(tview.Escape(raw), "\n") - fullFmt := strings.Replace(yamlFullFmt, "[key", "["+style.KeyColor.String(), 1) fullFmt = strings.Replace(fullFmt, "[colon", "["+style.ColonColor.String(), 1) fullFmt = strings.Replace(fullFmt, "[val", "["+style.ValueColor.String(), 1) @@ -60,20 +63,17 @@ func enableRegion(str string) string { return strings.ReplaceAll(strings.ReplaceAll(str, "<<<", "["), ">>>", "]") } -func saveYAML(screenDumpDir, context, name, data string) (string, error) { - dir := filepath.Join(screenDumpDir, context) +func saveYAML(dir, name, raw string) (string, error) { if err := ensureDir(dir); err != nil { return "", err } - now := time.Now().UnixNano() - fName := fmt.Sprintf("%s-%d.yml", config.SanitizeFilename(name), now) - - path := filepath.Join(dir, fName) + fName := fmt.Sprintf("%s--%d.yaml", data.SanitizeFileName(name), time.Now().Unix()) + fpath := filepath.Join(dir, fName) mod := os.O_CREATE | os.O_WRONLY - file, err := os.OpenFile(path, mod, 0600) + file, err := os.OpenFile(fpath, mod, 0600) if err != nil { - log.Error().Err(err).Msgf("YAML create %s", path) + log.Error().Err(err).Msgf("YAML create %s", fpath) return "", nil } defer func() { @@ -81,9 +81,9 @@ func saveYAML(screenDumpDir, context, name, data string) (string, error) { log.Error().Err(err).Msg("Closing yaml file") } }() - if _, err := file.Write([]byte(data)); err != nil { + if _, err := file.Write([]byte(raw)); err != nil { return "", err } - return path, nil + return fpath, nil } diff --git a/internal/view/yaml_test.go b/internal/view/yaml_test.go index 4c01a6a08f..c65d1a0e55 100644 --- a/internal/view/yaml_test.go +++ b/internal/view/yaml_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package view import ( diff --git a/internal/vul/scan.go b/internal/vul/scan.go new file mode 100644 index 0000000000..05819c305c --- /dev/null +++ b/internal/vul/scan.go @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package vul + +import ( + "fmt" + "io" + "strings" + + grypeDb "github.com/anchore/grype/grype/db/v5" + "github.com/anchore/grype/grype/match" + "github.com/anchore/grype/grype/vulnerability" +) + +const ( + wontFix = "(won't fix)" + naValue = "" +) + +// Scans tracks scans per image. +type Scans map[string]*Scan + +// Dump dump reports to stdout. +func (s Scans) Dump(w io.Writer) { + for k, v := range s { + fmt.Fprintf(w, "Image: %s -- ", k) + v.Tally.Dump(w) + fmt.Fprintln(w) + v.Dump(w) + } +} + +// Scan tracks image vulnerability scan. +type Scan struct { + ID string + Table *table + Tally tally +} + +func newScan(img string) *Scan { + return &Scan{ID: img, Table: newTable()} +} + +// Dump dump report to stdout. +func (s *Scan) Dump(w io.Writer) { + s.Table.dump(w) +} + +func (s *Scan) run(mm *match.Matches, store vulnerability.MetadataProvider) error { + for m := range mm.Enumerate() { + meta, err := store.GetMetadata(m.Vulnerability.ID, m.Vulnerability.Namespace) + if err != nil { + return err + } + var severity string + if meta != nil { + severity = meta.Severity + } + fixVersion := strings.Join(m.Vulnerability.Fix.Versions, ", ") + switch m.Vulnerability.Fix.State { + case grypeDb.WontFixState: + fixVersion = wontFix + case grypeDb.UnknownFixState: + fixVersion = naValue + } + s.Table.addRow(newRow(m.Package.Name, m.Package.Version, fixVersion, string(m.Package.Type), m.Vulnerability.ID, severity)) + } + s.Table.dedup() + s.Tally = newTally(s.Table) + + return nil +} + +func colorize(rr []string) []string { + crr := make([]string, len(rr)) + copy(crr, rr) + + crr[len(crr)-1] = sevColor(crr[len(crr)-1]) + return crr +} diff --git a/internal/vul/scanner.go b/internal/vul/scanner.go new file mode 100644 index 0000000000..82d3e47abb --- /dev/null +++ b/internal/vul/scanner.go @@ -0,0 +1,247 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package vul + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "github.com/derailed/k9s/internal/config" + "github.com/rs/zerolog/log" + + "github.com/anchore/clio" + "github.com/anchore/grype/cmd/grype/cli/options" + "github.com/anchore/grype/grype" + "github.com/anchore/grype/grype/db" + "github.com/anchore/grype/grype/matcher" + "github.com/anchore/grype/grype/matcher/dotnet" + "github.com/anchore/grype/grype/matcher/golang" + "github.com/anchore/grype/grype/matcher/java" + "github.com/anchore/grype/grype/matcher/javascript" + "github.com/anchore/grype/grype/matcher/python" + "github.com/anchore/grype/grype/matcher/ruby" + "github.com/anchore/grype/grype/matcher/stock" + "github.com/anchore/grype/grype/pkg" + "github.com/anchore/grype/grype/store" + "github.com/anchore/grype/grype/vex" + "github.com/anchore/syft/syft" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var ImgScanner *imageScanner + +const ( + imgChanSize = 3 + imgScanTimeout = 2 * time.Second + scanConcurrency = 2 +) + +type imageScanner struct { + store *store.Store + dbCloser *db.Closer + dbStatus *db.Status + opts *options.Grype + scans Scans + mx sync.RWMutex + initialized bool + config config.ImageScans +} + +// NewImageScanner returns a new instance. +func NewImageScanner(cfg config.ImageScans) *imageScanner { + return &imageScanner{ + scans: make(Scans), + config: cfg, + } +} + +func (s *imageScanner) ShouldExcludes(m metav1.ObjectMeta) bool { + return s.config.ShouldExclude(m.Namespace, m.Labels) +} + +// GetScan fetch scan for a given image. Returns ok=false when not found. +func (s *imageScanner) GetScan(img string) (*Scan, bool) { + s.mx.RLock() + defer s.mx.RUnlock() + + scan, ok := s.scans[img] + + return scan, ok +} + +func (s *imageScanner) setScan(img string, sc *Scan) { + s.mx.Lock() + defer s.mx.Unlock() + + s.scans[img] = sc +} + +// Init initializes image vulnerability database. +func (s *imageScanner) Init(name, version string) { + s.mx.Lock() + defer s.mx.Unlock() + + id := clio.Identification{Name: name, Version: version} + s.opts = options.DefaultGrype(id) + s.opts.GenerateMissingCPEs = true + + var err error + s.store, s.dbStatus, s.dbCloser, err = grype.LoadVulnerabilityDB( + s.opts.DB.ToCuratorConfig(), + s.opts.DB.AutoUpdate, + ) + if err != nil { + log.Error().Err(err).Msgf("VulDb load failed") + return + } + + if err := validateDBLoad(err, s.dbStatus); err != nil { + log.Error().Err(err).Msgf("VulDb validate failed") + return + } + + s.initialized = true +} + +// Stop closes scan database. +func (s *imageScanner) Stop() { + s.mx.RLock() + defer s.mx.RUnlock() + + if s.dbCloser != nil { + s.dbCloser.Close() + s.dbCloser = nil + } +} + +func (s *imageScanner) Score(ii ...string) string { + var sc scorer + for _, i := range ii { + if scan, ok := s.GetScan(i); ok { + sc = sc.Add(newScorer(scan.Tally)) + } + } + + return sc.String() +} + +func (s *imageScanner) isInitialized() bool { + s.mx.RLock() + defer s.mx.RUnlock() + + return s.initialized +} + +func (s *imageScanner) Enqueue(ctx context.Context, images ...string) { + if !s.isInitialized() { + return + } + ctx, cancel := context.WithTimeout(ctx, imgScanTimeout) + defer cancel() + + for _, img := range images { + if _, ok := s.GetScan(img); ok { + continue + } + go s.scanWorker(ctx, img) + } +} + +func (s *imageScanner) scanWorker(ctx context.Context, img string) { + defer log.Debug().Msgf("ScanWorker bailing out!") + + log.Debug().Msgf("ScanWorker processing: %q", img) + sc := newScan(img) + s.setScan(img, sc) + if err := s.scan(ctx, img, sc); err != nil { + log.Warn().Err(err).Msgf("Scan failed for img %s --", img) + } +} + +func (s *imageScanner) scan(ctx context.Context, img string, sc *Scan) error { + defer func(t time.Time) { + log.Debug().Msgf("ScanTime %q: %v", img, time.Since(t)) + }(time.Now()) + + var errs error + packages, pkgContext, _, err := pkg.Provide(img, getProviderConfig(s.opts)) + if err != nil { + errs = errors.Join(errs, fmt.Errorf("failed to catalog %s: %w", img, err)) + } + + v := grype.VulnerabilityMatcher{ + Store: *s.store, + IgnoreRules: s.opts.Ignore, + NormalizeByCVE: s.opts.ByCVE, + FailSeverity: s.opts.FailOnSeverity(), + Matchers: getMatchers(s.opts), + VexProcessor: vex.NewProcessor(vex.ProcessorOptions{ + Documents: s.opts.VexDocuments, + IgnoreRules: s.opts.Ignore, + }), + } + + mm, _, err := v.FindMatches(packages, pkgContext) + if err != nil { + errs = errors.Join(errs, err) + } + if err := sc.run(mm, s.store); err != nil { + errs = errors.Join(errs, err) + } + + return errs +} + +func getProviderConfig(opts *options.Grype) pkg.ProviderConfig { + return pkg.ProviderConfig{ + SyftProviderConfig: pkg.SyftProviderConfig{ + SBOMOptions: syft.DefaultCreateSBOMConfig(), + RegistryOptions: opts.Registry.ToOptions(), + Exclusions: opts.Exclusions, + Platform: opts.Platform, + Name: opts.Name, + DefaultImagePullSource: opts.DefaultImagePullSource, + }, + SynthesisConfig: pkg.SynthesisConfig{ + GenerateMissingCPEs: opts.GenerateMissingCPEs, + }, + } +} + +func getMatchers(opts *options.Grype) []matcher.Matcher { + return matcher.NewDefaultMatchers( + matcher.Config{ + Java: java.MatcherConfig{ + ExternalSearchConfig: opts.ExternalSources.ToJavaMatcherConfig(), + UseCPEs: opts.Match.Java.UseCPEs, + }, + Ruby: ruby.MatcherConfig(opts.Match.Ruby), + Python: python.MatcherConfig(opts.Match.Python), + Dotnet: dotnet.MatcherConfig(opts.Match.Dotnet), + Javascript: javascript.MatcherConfig(opts.Match.Javascript), + Golang: golang.MatcherConfig{ + UseCPEs: opts.Match.Golang.UseCPEs, + AlwaysUseCPEForStdlib: opts.Match.Golang.AlwaysUseCPEForStdlib, + }, + Stock: stock.MatcherConfig(opts.Match.Stock), + }, + ) +} + +func validateDBLoad(loadErr error, status *db.Status) error { + if loadErr != nil { + return fmt.Errorf("failed to load vulnerability db: %w", loadErr) + } + if status == nil { + return fmt.Errorf("unable to determine the status of the vulnerability db") + } + if status.Err != nil { + return fmt.Errorf("db could not be loaded: %w", status.Err) + } + + return nil +} diff --git a/internal/vul/scorer.go b/internal/vul/scorer.go new file mode 100644 index 0000000000..30e949a8d9 --- /dev/null +++ b/internal/vul/scorer.go @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package vul + +import "fmt" + +type scorer uint8 + +func (b scorer) String() string { + return fmt.Sprintf("%08b", b)[:6] +} + +func newScorer(t tally) scorer { + return fromTally(t) +} + +func (b scorer) Add(b1 scorer) scorer { + return b | b1 +} + +func fromTally(t tally) scorer { + var b scorer + for i, v := range t { + if v == 0 { + continue + } + switch i { + case sevCritical: + b |= 0x80 + case sevHigh: + b |= 0x40 + case sevMedium: + b |= 0x20 + case sevLow: + b |= 0x10 + case sevNegligible: + b |= 0x08 + case sevUnknown: + b |= 0x04 + } + } + + return b +} diff --git a/internal/vul/scorer_test.go b/internal/vul/scorer_test.go new file mode 100644 index 0000000000..ac6304cc7c --- /dev/null +++ b/internal/vul/scorer_test.go @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package vul + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_scorerAdd(t *testing.T) { + uu := map[string]struct { + b, b1, e scorer + }{ + "zero": {}, + "same": { + b: scorer(0x80), + b1: scorer(0x80), + e: scorer(0x80), + }, + "c+h": { + b: scorer(0x80), + b1: scorer(0x40), + e: scorer(0xC0), + }, + "ch+hm": { + b: scorer(0xc0), + b1: scorer(0xa0), + e: scorer(0xe0), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.e, u.b.Add(u.b1)) + }) + } +} + +func Test_scorerFromTally(t *testing.T) { + uu := map[string]struct { + tt tally + b scorer + }{ + "zero": {}, + "critical": { + tt: tally{29, 0, 0, 0, 0, 0, 0}, + b: scorer(0x80), + }, + "high": { + tt: tally{0, 17, 0, 0, 0, 0, 0}, + b: scorer(0x40), + }, + "medium": { + tt: tally{0, 0, 5, 0, 0, 0, 0}, + b: scorer(0x20), + }, + "low": { + tt: tally{0, 0, 0, 10, 0, 0, 0}, + b: scorer(0x10), + }, + "negligible": { + tt: tally{0, 0, 0, 0, 10, 0, 0}, + b: scorer(0x08), + }, + "unknown": { + tt: tally{0, 0, 0, 0, 0, 10, 0}, + b: scorer(0x04), + }, + "c/h": { + tt: tally{10, 20, 0, 0, 0, 0, 0}, + b: scorer(0xC0), + }, + "c/m": { + tt: tally{10, 0, 20, 0, 0, 0, 0}, + b: scorer(0xA0), + }, + "c/h/l": { + tt: tally{10, 1, 20, 0, 0, 0, 0}, + b: scorer(0xE0), + }, + "n/u": { + tt: tally{0, 0, 0, 0, 10, 20, 0}, + b: scorer(0x0C), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.b, newScorer(u.tt)) + }) + } +} diff --git a/internal/vul/table.go b/internal/vul/table.go new file mode 100644 index 0000000000..6b3dbf4e5e --- /dev/null +++ b/internal/vul/table.go @@ -0,0 +1,184 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package vul + +import ( + "fmt" + "io" + "sort" + "strings" + + "github.com/olekukonko/tablewriter" +) + +const ( + nameIdx = iota + verIdx + fixIdx + typeIdx + vulIdx + sevIdx +) + +type Row []string + +func newRow(ss ...string) Row { + r := make(Row, 0, len(ss)) + for i, s := range ss { + if i == sevIdx { + s = toSev(s) + } + r = append(r, s) + } + return r +} + +func toSev(s string) string { + switch s { + case "Critical": + return Sev1 + case "High": + return Sev2 + case "Medium": + return Sev3 + case "Low": + return Sev4 + case "Negligible": + return Sev5 + default: + return SevU + } +} + +func (r Row) Name() string { return r[nameIdx] } +func (r Row) Version() string { return r[verIdx] } +func (r Row) Fix() string { return r[fixIdx] } +func (r Row) Type() string { return r[typeIdx] } +func (r Row) Vulnerability() string { return r[vulIdx] } +func (r Row) Severity() string { return r[sevIdx] } + +func sevColor(s string) string { + switch strings.ToLower(s) { + case "critical": + return fmt.Sprintf("[red::b]%s[-::-]", s) + case "high": + return fmt.Sprintf("[orange::b]%s[-::-]", s) + case "medium": + return fmt.Sprintf("[yellow::b]%s[-::-]", s) + case "low": + return fmt.Sprintf("[blue::b]%s[-::-]", s) + default: + return fmt.Sprintf("[gray::b]%s[-::-]", s) + } +} + +type table struct { + Rows []Row +} + +func newTable() *table { + return &table{} +} + +func (t *table) dedup() { + var ( + seen = make(map[string]struct{}, len(t.Rows)) + rr = make([]Row, 0, len(t.Rows)) + ) + for _, v := range t.Rows { + key := strings.Join(v, "|") + if _, ok := seen[key]; ok { + continue + } + rr, seen[key] = append(rr, v), struct{}{} + } + t.Rows = rr +} + +func (t *table) addRow(r Row) { + t.Rows = append(t.Rows, r) +} + +func (t *table) dump(w io.Writer) { + columns := []string{"Name", "Installed", "Fixed-In", "Type", "Vulnerability", "Severity"} + + table := tablewriter.NewWriter(w) + table.SetHeader(columns) + table.SetAutoWrapText(false) + table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) + table.SetAlignment(tablewriter.ALIGN_LEFT) + + table.SetHeaderLine(false) + table.SetBorder(false) + table.SetAutoFormatHeaders(true) + table.SetCenterSeparator("") + table.SetColumnSeparator("") + table.SetRowSeparator("") + table.SetTablePadding(" ") + table.SetNoWhiteSpace(true) + + for _, row := range t.Rows { + table.Append(colorize(row)) + } + table.Render() +} + +func (t *table) sort() { + t.dedup() + + sort.SliceStable(t.Rows, func(i, j int) bool { + if t.Rows[i][nameIdx] != t.Rows[j][nameIdx] { + return t.Rows[i][nameIdx] < t.Rows[j][nameIdx] + } + if t.Rows[i][verIdx] != t.Rows[j][verIdx] { + return t.Rows[i][verIdx] < t.Rows[j][verIdx] + } + if t.Rows[i][typeIdx] != t.Rows[j][typeIdx] { + return t.Rows[i][typeIdx] < t.Rows[j][typeIdx] + } + + if t.Rows[i][sevIdx] == t.Rows[j][sevIdx] { + return t.Rows[i][vulIdx] < t.Rows[j][vulIdx] + } + return sevToScore(t.Rows[i][sevIdx]) < sevToScore(t.Rows[j][sevIdx]) + }) +} + +func (t *table) sortSev() { + t.dedup() + + sort.SliceStable(t.Rows, func(i, j int) bool { + if s1, s2 := sevToScore(t.Rows[i][sevIdx]), sevToScore(t.Rows[j][sevIdx]); s1 != s2 { + return s1 < s2 + } + if t.Rows[i][nameIdx] != t.Rows[j][nameIdx] { + return t.Rows[i][nameIdx] < t.Rows[j][nameIdx] + } + if t.Rows[i][verIdx] != t.Rows[j][verIdx] { + return t.Rows[i][verIdx] < t.Rows[j][verIdx] + } + if t.Rows[i][typeIdx] != t.Rows[j][typeIdx] { + return t.Rows[i][typeIdx] < t.Rows[j][typeIdx] + } + + return t.Rows[i][vulIdx] < t.Rows[j][vulIdx] + }) +} + +func sevToScore(s string) int { + switch s { + case Sev1: + return 1 + case Sev2: + return 2 + case Sev3: + return 3 + case Sev4: + return 4 + case Sev5: + return 5 + default: + return 6 + } +} diff --git a/internal/vul/table_test.go b/internal/vul/table_test.go new file mode 100644 index 0000000000..b8e5b2f8e6 --- /dev/null +++ b/internal/vul/table_test.go @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package vul + +import ( + "bufio" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_sort(t *testing.T) { + uu := map[string]struct { + t1, t2 *table + }{ + "simple": { + t1: makeTable(t, "testdata/sort/no_dups/sc1.text"), + t2: makeTable(t, "testdata/sort/no_dups/sc2.text"), + }, + "dups": { + t1: makeTable(t, "testdata/sort/dups/sc1.text"), + t2: makeTable(t, "testdata/sort/dups/sc2.text"), + }, + "full": { + t1: makeTable(t, "testdata/sort/full/sc1.text"), + t2: makeTable(t, "testdata/sort/full/sc2.text"), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + u.t1.sort() + assert.Equal(t, u.t2, u.t1) + }) + } +} + +func Test_sortSev(t *testing.T) { + uu := map[string]struct { + t1, t2 *table + }{ + "simple": { + t1: makeTable(t, "testdata/sort_sev/no_dups/sc1.text"), + t2: makeTable(t, "testdata/sort_sev/no_dups/sc2.text"), + }, + "dups": { + t1: makeTable(t, "testdata/sort_sev/dups/sc1.text"), + t2: makeTable(t, "testdata/sort_sev/dups/sc2.text"), + }, + "full": { + t1: makeTable(t, "testdata/sort_sev/full/sc1.text"), + t2: makeTable(t, "testdata/sort_sev/full/sc2.text"), + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + u.t1.sortSev() + assert.Equal(t, u.t2, u.t1) + }) + } +} + +// Helpers... + +func makeTable(t *testing.T, path string) *table { + f, err := os.Open(path) + defer func() { + _ = f.Close() + }() + assert.NoError(t, err) + sc := bufio.NewScanner(f) + var tt table + for sc.Scan() { + ff := strings.Fields(sc.Text()) + tt.addRow(newRow(ff...)) + } + assert.NoError(t, sc.Err()) + + return &tt +} diff --git a/internal/vul/tally.go b/internal/vul/tally.go new file mode 100644 index 0000000000..a7ca25e8ed --- /dev/null +++ b/internal/vul/tally.go @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package vul + +import ( + "fmt" + "io" +) + +const ( + sevCritical = iota + sevHigh + sevMedium + sevLow + sevNegligible + sevUnknown + sevFixed +) + +var vulWeights = []int{10_000, 100, 100, 10, 0, 0, 0, 0} + +type tally [7]int + +func newTally(t *table) tally { + var tt tally + for _, r := range t.Rows { + if r.Fix() != "" { + tt[sevFixed]++ + } + switch r.Severity() { + case Sev1: + tt[sevCritical]++ + case Sev2: + tt[sevHigh]++ + case Sev3: + tt[sevMedium]++ + case Sev4: + tt[sevLow]++ + case Sev5: + tt[sevNegligible]++ + case SevU: + tt[sevUnknown]++ + } + } + + return tt +} + +// Dump dumps tally as text. +func (t tally) Dump(w io.Writer) { + fmt.Fprintf(w, "%d critical, %d high, %d medium, %d low, %d negligible", + t[sevCritical], + t[sevHigh], + t[sevMedium], + t[sevLow], + t[sevNegligible], + ) + if t[sevUnknown] > 0 { + fmt.Fprintf(w, " (%d unknown)", t[sevUnknown]) + } + if t[sevFixed] > 0 { + fmt.Fprintf(w, " -- [Fixed: %d]", t[sevFixed]) + } +} + +func (t *tally) score() int { + var s int + for i, v := range t[:5] { + s += v * vulWeights[i] + } + + return s +} diff --git a/internal/vul/tally_test.go b/internal/vul/tally_test.go new file mode 100644 index 0000000000..3179ca0b51 --- /dev/null +++ b/internal/vul/tally_test.go @@ -0,0 +1,57 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package vul + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_newTally(t *testing.T) { + uu := map[string]struct { + t *table + tt tally + }{ + "full": { + t: makeTable(t, "testdata/sort/full/sc2.text"), + tt: tally{7, 14, 8, 0, 0, 0, 29}, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.tt, newTally(u.t)) + }) + } +} + +func Test_score(t *testing.T) { + uu := map[string]struct { + tt tally + sc int + }{ + "zero": {}, + "critical": { + tt: tally{29, 7, 14, 8, 0, 0, 0}, + sc: 292180, + }, + "high": { + tt: tally{0, 17, 14, 8, 0, 0, 0}, + sc: 3180, + }, + "medium": { + tt: tally{0, 0, 14, 0, 0, 0, 0}, + sc: 1400, + }, + } + + for k := range uu { + u := uu[k] + t.Run(k, func(t *testing.T) { + assert.Equal(t, u.sc, u.tt.score()) + }) + } +} diff --git a/internal/vul/testdata/sort/dups/sc1.text b/internal/vul/testdata/sort/dups/sc1.text new file mode 100644 index 0000000000..600a7a17e0 --- /dev/null +++ b/internal/vul/testdata/sort/dups/sc1.text @@ -0,0 +1,9 @@ +busybox 1.34.1 n/a binary CVE-2022-48174 Critical +busybox 1.34.1 n/a binary CVE-2022-28391 High +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-4374-p667-p6c8 High +golang.org/x/net v0.4.0 0.7.0 go-module GHSA-vvpx-j8f3-3w6h High +golang.org/x/net v0.4.0 0.7.0 go-module GHSA-vvpx-j8f3-3w6h High +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-qppj-fm5r-hxr3 Medium +golang.org/x/net v0.4.0 0.13.0 go-module GHSA-2wrh-6pvc-2jm9 Medium +golang.org/x/net v0.4.0 0.13.0 go-module GHSA-2wrh-6pvc-2jm9 Medium +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-qppj-fm5r-hxr3 Medium \ No newline at end of file diff --git a/internal/vul/testdata/sort/dups/sc2.text b/internal/vul/testdata/sort/dups/sc2.text new file mode 100644 index 0000000000..38df5858cf --- /dev/null +++ b/internal/vul/testdata/sort/dups/sc2.text @@ -0,0 +1,6 @@ +busybox 1.34.1 n/a binary CVE-2022-48174 Critical +busybox 1.34.1 n/a binary CVE-2022-28391 High +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-4374-p667-p6c8 High +golang.org/x/net v0.4.0 0.7.0 go-module GHSA-vvpx-j8f3-3w6h High +golang.org/x/net v0.4.0 0.13.0 go-module GHSA-2wrh-6pvc-2jm9 Medium +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-qppj-fm5r-hxr3 Medium \ No newline at end of file diff --git a/internal/vul/testdata/sort/full/sc1.text b/internal/vul/testdata/sort/full/sc1.text new file mode 100644 index 0000000000..75ef53c497 --- /dev/null +++ b/internal/vul/testdata/sort/full/sc1.text @@ -0,0 +1,56 @@ +busybox 1.34.1 n/a binary CVE-2022-48174 Critical +busybox 1.34.1 n/a binary CVE-2022-28391 High +github.com/prometheus/alertmanager v0.25.0 0.25.1 go-module GHSA-v86x-5fm3-5p7j Medium +github.com/prometheus/alertmanager v0.25.0 0.25.1 go-module GHSA-v86x-5fm3-5p7j Medium +golang.org/x/net v0.4.0 0.7.0 go-module GHSA-vvpx-j8f3-3w6h High +golang.org/x/net v0.4.0 0.7.0 go-module GHSA-vvpx-j8f3-3w6h High +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-4374-p667-p6c8 High +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-4374-p667-p6c8 High +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-qppj-fm5r-hxr3 Medium +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-qppj-fm5r-hxr3 Medium +golang.org/x/net v0.4.0 0.13.0 go-module GHSA-2wrh-6pvc-2jm9 Medium +golang.org/x/net v0.4.0 0.13.0 go-module GHSA-2wrh-6pvc-2jm9 Medium +stdlib go1.19.4 n/a go-module CVE-2023-29404 Critical +stdlib go1.19.4 n/a go-module CVE-2023-39323 Critical +stdlib go1.19.4 n/a go-module CVE-2023-24538 Critical +stdlib go1.19.4 n/a go-module CVE-2023-29405 Critical +stdlib go1.19.4 n/a go-module CVE-2023-24540 Critical +stdlib go1.19.4 n/a go-module CVE-2023-29405 Critical +stdlib go1.19.4 n/a go-module CVE-2023-24540 Critical +stdlib go1.19.4 n/a go-module CVE-2023-24538 Critical +stdlib go1.19.4 n/a go-module CVE-2023-29402 Critical +stdlib go1.19.4 n/a go-module CVE-2023-29402 Critical +stdlib go1.19.4 n/a go-module CVE-2023-29404 Critical +stdlib go1.19.4 n/a go-module CVE-2023-39323 Critical +stdlib go1.19.4 n/a go-module CVE-2022-41724 High +stdlib go1.19.4 n/a go-module CVE-2022-41725 High +stdlib go1.19.4 n/a go-module CVE-2023-24534 High +stdlib go1.19.4 n/a go-module CVE-2023-29400 High +stdlib go1.19.4 n/a go-module CVE-2023-24539 High +stdlib go1.19.4 n/a go-module CVE-2023-29403 High +stdlib go1.19.4 n/a go-module CVE-2023-44487 High +stdlib go1.19.4 n/a go-module CVE-2022-41722 High +stdlib go1.19.4 n/a go-module CVE-2022-41724 High +stdlib go1.19.4 n/a go-module CVE-2022-41723 High +stdlib go1.19.4 n/a go-module CVE-2023-24534 High +stdlib go1.19.4 n/a go-module CVE-2022-41725 High +stdlib go1.19.4 n/a go-module CVE-2023-24536 High +stdlib go1.19.4 n/a go-module CVE-2023-24537 High +stdlib go1.19.4 n/a go-module CVE-2023-24537 High +stdlib go1.19.4 n/a go-module CVE-2022-41723 High +stdlib go1.19.4 n/a go-module CVE-2023-24536 High +stdlib go1.19.4 n/a go-module CVE-2023-29403 High +stdlib go1.19.4 n/a go-module CVE-2023-29400 High +stdlib go1.19.4 n/a go-module CVE-2022-41722 High +stdlib go1.19.4 n/a go-module CVE-2023-24539 High +stdlib go1.19.4 n/a go-module CVE-2023-44487 High +stdlib go1.19.4 n/a go-module CVE-2023-29406 Medium +stdlib go1.19.4 n/a go-module CVE-2023-29409 Medium +stdlib go1.19.4 n/a go-module CVE-2023-29409 Medium +stdlib go1.19.4 n/a go-module CVE-2023-24532 Medium +stdlib go1.19.4 n/a go-module CVE-2023-39319 Medium +stdlib go1.19.4 n/a go-module CVE-2023-24532 Medium +stdlib go1.19.4 n/a go-module CVE-2023-29406 Medium +stdlib go1.19.4 n/a go-module CVE-2023-39318 Medium +stdlib go1.19.4 n/a go-module CVE-2023-39319 Medium +stdlib go1.19.4 n/a go-module CVE-2023-39318 Medium \ No newline at end of file diff --git a/internal/vul/testdata/sort/full/sc2.text b/internal/vul/testdata/sort/full/sc2.text new file mode 100644 index 0000000000..74ffd85abd --- /dev/null +++ b/internal/vul/testdata/sort/full/sc2.text @@ -0,0 +1,29 @@ +busybox 1.34.1 n/a binary CVE-2022-48174 Critical +busybox 1.34.1 n/a binary CVE-2022-28391 High +github.com/prometheus/alertmanager v0.25.0 0.25.1 go-module GHSA-v86x-5fm3-5p7j Medium +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-4374-p667-p6c8 High +golang.org/x/net v0.4.0 0.7.0 go-module GHSA-vvpx-j8f3-3w6h High +golang.org/x/net v0.4.0 0.13.0 go-module GHSA-2wrh-6pvc-2jm9 Medium +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-qppj-fm5r-hxr3 Medium +stdlib go1.19.4 n/a go-module CVE-2023-24538 Critical +stdlib go1.19.4 n/a go-module CVE-2023-24540 Critical +stdlib go1.19.4 n/a go-module CVE-2023-29402 Critical +stdlib go1.19.4 n/a go-module CVE-2023-29404 Critical +stdlib go1.19.4 n/a go-module CVE-2023-29405 Critical +stdlib go1.19.4 n/a go-module CVE-2023-39323 Critical +stdlib go1.19.4 n/a go-module CVE-2022-41722 High +stdlib go1.19.4 n/a go-module CVE-2022-41723 High +stdlib go1.19.4 n/a go-module CVE-2022-41724 High +stdlib go1.19.4 n/a go-module CVE-2022-41725 High +stdlib go1.19.4 n/a go-module CVE-2023-24534 High +stdlib go1.19.4 n/a go-module CVE-2023-24536 High +stdlib go1.19.4 n/a go-module CVE-2023-24537 High +stdlib go1.19.4 n/a go-module CVE-2023-24539 High +stdlib go1.19.4 n/a go-module CVE-2023-29400 High +stdlib go1.19.4 n/a go-module CVE-2023-29403 High +stdlib go1.19.4 n/a go-module CVE-2023-44487 High +stdlib go1.19.4 n/a go-module CVE-2023-24532 Medium +stdlib go1.19.4 n/a go-module CVE-2023-29406 Medium +stdlib go1.19.4 n/a go-module CVE-2023-29409 Medium +stdlib go1.19.4 n/a go-module CVE-2023-39318 Medium +stdlib go1.19.4 n/a go-module CVE-2023-39319 Medium \ No newline at end of file diff --git a/internal/vul/testdata/sort/no_dups/sc1.text b/internal/vul/testdata/sort/no_dups/sc1.text new file mode 100644 index 0000000000..6190c71713 --- /dev/null +++ b/internal/vul/testdata/sort/no_dups/sc1.text @@ -0,0 +1,2 @@ +busybox 1.34.1 n/a binary CVE-2022-48174 Critical +busybox 1.34.1 n/a binary CVE-2022-28391 High \ No newline at end of file diff --git a/internal/vul/testdata/sort/no_dups/sc2.text b/internal/vul/testdata/sort/no_dups/sc2.text new file mode 100644 index 0000000000..f9f06da82c --- /dev/null +++ b/internal/vul/testdata/sort/no_dups/sc2.text @@ -0,0 +1,2 @@ +busybox 1.34.1 n/a binary CVE-2022-48174 Critical +busybox 1.34.1 n/a binary CVE-2022-28391 High \ No newline at end of file diff --git a/internal/vul/testdata/sort_sev/dups/sc1.text b/internal/vul/testdata/sort_sev/dups/sc1.text new file mode 100644 index 0000000000..600a7a17e0 --- /dev/null +++ b/internal/vul/testdata/sort_sev/dups/sc1.text @@ -0,0 +1,9 @@ +busybox 1.34.1 n/a binary CVE-2022-48174 Critical +busybox 1.34.1 n/a binary CVE-2022-28391 High +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-4374-p667-p6c8 High +golang.org/x/net v0.4.0 0.7.0 go-module GHSA-vvpx-j8f3-3w6h High +golang.org/x/net v0.4.0 0.7.0 go-module GHSA-vvpx-j8f3-3w6h High +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-qppj-fm5r-hxr3 Medium +golang.org/x/net v0.4.0 0.13.0 go-module GHSA-2wrh-6pvc-2jm9 Medium +golang.org/x/net v0.4.0 0.13.0 go-module GHSA-2wrh-6pvc-2jm9 Medium +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-qppj-fm5r-hxr3 Medium \ No newline at end of file diff --git a/internal/vul/testdata/sort_sev/dups/sc2.text b/internal/vul/testdata/sort_sev/dups/sc2.text new file mode 100644 index 0000000000..38df5858cf --- /dev/null +++ b/internal/vul/testdata/sort_sev/dups/sc2.text @@ -0,0 +1,6 @@ +busybox 1.34.1 n/a binary CVE-2022-48174 Critical +busybox 1.34.1 n/a binary CVE-2022-28391 High +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-4374-p667-p6c8 High +golang.org/x/net v0.4.0 0.7.0 go-module GHSA-vvpx-j8f3-3w6h High +golang.org/x/net v0.4.0 0.13.0 go-module GHSA-2wrh-6pvc-2jm9 Medium +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-qppj-fm5r-hxr3 Medium \ No newline at end of file diff --git a/internal/vul/testdata/sort_sev/full/sc1.text b/internal/vul/testdata/sort_sev/full/sc1.text new file mode 100644 index 0000000000..75ef53c497 --- /dev/null +++ b/internal/vul/testdata/sort_sev/full/sc1.text @@ -0,0 +1,56 @@ +busybox 1.34.1 n/a binary CVE-2022-48174 Critical +busybox 1.34.1 n/a binary CVE-2022-28391 High +github.com/prometheus/alertmanager v0.25.0 0.25.1 go-module GHSA-v86x-5fm3-5p7j Medium +github.com/prometheus/alertmanager v0.25.0 0.25.1 go-module GHSA-v86x-5fm3-5p7j Medium +golang.org/x/net v0.4.0 0.7.0 go-module GHSA-vvpx-j8f3-3w6h High +golang.org/x/net v0.4.0 0.7.0 go-module GHSA-vvpx-j8f3-3w6h High +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-4374-p667-p6c8 High +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-4374-p667-p6c8 High +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-qppj-fm5r-hxr3 Medium +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-qppj-fm5r-hxr3 Medium +golang.org/x/net v0.4.0 0.13.0 go-module GHSA-2wrh-6pvc-2jm9 Medium +golang.org/x/net v0.4.0 0.13.0 go-module GHSA-2wrh-6pvc-2jm9 Medium +stdlib go1.19.4 n/a go-module CVE-2023-29404 Critical +stdlib go1.19.4 n/a go-module CVE-2023-39323 Critical +stdlib go1.19.4 n/a go-module CVE-2023-24538 Critical +stdlib go1.19.4 n/a go-module CVE-2023-29405 Critical +stdlib go1.19.4 n/a go-module CVE-2023-24540 Critical +stdlib go1.19.4 n/a go-module CVE-2023-29405 Critical +stdlib go1.19.4 n/a go-module CVE-2023-24540 Critical +stdlib go1.19.4 n/a go-module CVE-2023-24538 Critical +stdlib go1.19.4 n/a go-module CVE-2023-29402 Critical +stdlib go1.19.4 n/a go-module CVE-2023-29402 Critical +stdlib go1.19.4 n/a go-module CVE-2023-29404 Critical +stdlib go1.19.4 n/a go-module CVE-2023-39323 Critical +stdlib go1.19.4 n/a go-module CVE-2022-41724 High +stdlib go1.19.4 n/a go-module CVE-2022-41725 High +stdlib go1.19.4 n/a go-module CVE-2023-24534 High +stdlib go1.19.4 n/a go-module CVE-2023-29400 High +stdlib go1.19.4 n/a go-module CVE-2023-24539 High +stdlib go1.19.4 n/a go-module CVE-2023-29403 High +stdlib go1.19.4 n/a go-module CVE-2023-44487 High +stdlib go1.19.4 n/a go-module CVE-2022-41722 High +stdlib go1.19.4 n/a go-module CVE-2022-41724 High +stdlib go1.19.4 n/a go-module CVE-2022-41723 High +stdlib go1.19.4 n/a go-module CVE-2023-24534 High +stdlib go1.19.4 n/a go-module CVE-2022-41725 High +stdlib go1.19.4 n/a go-module CVE-2023-24536 High +stdlib go1.19.4 n/a go-module CVE-2023-24537 High +stdlib go1.19.4 n/a go-module CVE-2023-24537 High +stdlib go1.19.4 n/a go-module CVE-2022-41723 High +stdlib go1.19.4 n/a go-module CVE-2023-24536 High +stdlib go1.19.4 n/a go-module CVE-2023-29403 High +stdlib go1.19.4 n/a go-module CVE-2023-29400 High +stdlib go1.19.4 n/a go-module CVE-2022-41722 High +stdlib go1.19.4 n/a go-module CVE-2023-24539 High +stdlib go1.19.4 n/a go-module CVE-2023-44487 High +stdlib go1.19.4 n/a go-module CVE-2023-29406 Medium +stdlib go1.19.4 n/a go-module CVE-2023-29409 Medium +stdlib go1.19.4 n/a go-module CVE-2023-29409 Medium +stdlib go1.19.4 n/a go-module CVE-2023-24532 Medium +stdlib go1.19.4 n/a go-module CVE-2023-39319 Medium +stdlib go1.19.4 n/a go-module CVE-2023-24532 Medium +stdlib go1.19.4 n/a go-module CVE-2023-29406 Medium +stdlib go1.19.4 n/a go-module CVE-2023-39318 Medium +stdlib go1.19.4 n/a go-module CVE-2023-39319 Medium +stdlib go1.19.4 n/a go-module CVE-2023-39318 Medium \ No newline at end of file diff --git a/internal/vul/testdata/sort_sev/full/sc2.text b/internal/vul/testdata/sort_sev/full/sc2.text new file mode 100644 index 0000000000..4e513f4cdc --- /dev/null +++ b/internal/vul/testdata/sort_sev/full/sc2.text @@ -0,0 +1,29 @@ +busybox 1.34.1 n/a binary CVE-2022-48174 Critical +stdlib go1.19.4 n/a go-module CVE-2023-24538 Critical +stdlib go1.19.4 n/a go-module CVE-2023-24540 Critical +stdlib go1.19.4 n/a go-module CVE-2023-29402 Critical +stdlib go1.19.4 n/a go-module CVE-2023-29404 Critical +stdlib go1.19.4 n/a go-module CVE-2023-29405 Critical +stdlib go1.19.4 n/a go-module CVE-2023-39323 Critical +busybox 1.34.1 n/a binary CVE-2022-28391 High +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-4374-p667-p6c8 High +golang.org/x/net v0.4.0 0.7.0 go-module GHSA-vvpx-j8f3-3w6h High +stdlib go1.19.4 n/a go-module CVE-2022-41722 High +stdlib go1.19.4 n/a go-module CVE-2022-41723 High +stdlib go1.19.4 n/a go-module CVE-2022-41724 High +stdlib go1.19.4 n/a go-module CVE-2022-41725 High +stdlib go1.19.4 n/a go-module CVE-2023-24534 High +stdlib go1.19.4 n/a go-module CVE-2023-24536 High +stdlib go1.19.4 n/a go-module CVE-2023-24537 High +stdlib go1.19.4 n/a go-module CVE-2023-24539 High +stdlib go1.19.4 n/a go-module CVE-2023-29400 High +stdlib go1.19.4 n/a go-module CVE-2023-29403 High +stdlib go1.19.4 n/a go-module CVE-2023-44487 High +github.com/prometheus/alertmanager v0.25.0 0.25.1 go-module GHSA-v86x-5fm3-5p7j Medium +golang.org/x/net v0.4.0 0.13.0 go-module GHSA-2wrh-6pvc-2jm9 Medium +golang.org/x/net v0.4.0 0.17.0 go-module GHSA-qppj-fm5r-hxr3 Medium +stdlib go1.19.4 n/a go-module CVE-2023-24532 Medium +stdlib go1.19.4 n/a go-module CVE-2023-29406 Medium +stdlib go1.19.4 n/a go-module CVE-2023-29409 Medium +stdlib go1.19.4 n/a go-module CVE-2023-39318 Medium +stdlib go1.19.4 n/a go-module CVE-2023-39319 Medium \ No newline at end of file diff --git a/internal/vul/testdata/sort_sev/no_dups/sc1.text b/internal/vul/testdata/sort_sev/no_dups/sc1.text new file mode 100644 index 0000000000..6190c71713 --- /dev/null +++ b/internal/vul/testdata/sort_sev/no_dups/sc1.text @@ -0,0 +1,2 @@ +busybox 1.34.1 n/a binary CVE-2022-48174 Critical +busybox 1.34.1 n/a binary CVE-2022-28391 High \ No newline at end of file diff --git a/internal/vul/testdata/sort_sev/no_dups/sc2.text b/internal/vul/testdata/sort_sev/no_dups/sc2.text new file mode 100644 index 0000000000..f9f06da82c --- /dev/null +++ b/internal/vul/testdata/sort_sev/no_dups/sc2.text @@ -0,0 +1,2 @@ +busybox 1.34.1 n/a binary CVE-2022-48174 Critical +busybox 1.34.1 n/a binary CVE-2022-28391 High \ No newline at end of file diff --git a/internal/vul/types.go b/internal/vul/types.go new file mode 100644 index 0000000000..339714d46c --- /dev/null +++ b/internal/vul/types.go @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + +package vul + +const ( + // Sev1 tracks Critical sev. + Sev1 = "SEV-1" + + // Sev2 tracks High sev. + Sev2 = "SEV-2" + + // Sev3 tracks Medium sev. + Sev3 = "SEV-3" + + // Sev4 tracks Low sev. + Sev4 = "SEV-4" + + // Sev5 tracks Negligible sev. + Sev5 = "SEV-5" + + // SevU tracks Unknown sev. + SevU = "SEV-U" +) diff --git a/internal/watch/factory.go b/internal/watch/factory.go index 5d358e6d63..6c896afa6d 100644 --- a/internal/watch/factory.go +++ b/internal/watch/factory.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package watch import ( @@ -8,6 +11,8 @@ import ( "github.com/derailed/k9s/internal/client" "github.com/rs/zerolog/log" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" di "k8s.io/client-go/dynamic/dynamicinformer" @@ -67,12 +72,12 @@ func (f *Factory) Terminate() { // List returns a resource collection. func (f *Factory) List(gvr, ns string, wait bool, labels labels.Selector) ([]runtime.Object, error) { - inf, err := f.CanForResource(ns, gvr, client.MonitorAccess) + inf, err := f.CanForResource(ns, gvr, client.ListAccess) if err != nil { return nil, err } if client.IsAllNamespace(ns) { - ns = client.AllNamespaces + ns = client.BlankNamespace } var oo []runtime.Object @@ -94,7 +99,7 @@ func (f *Factory) List(gvr, ns string, wait bool, labels labels.Selector) ([]run // HasSynced checks if given informer is up to date. func (f *Factory) HasSynced(gvr, ns string) (bool, error) { - inf, err := f.CanForResource(ns, gvr, client.MonitorAccess) + inf, err := f.CanForResource(ns, gvr, client.ListAccess) if err != nil { return false, err } @@ -128,7 +133,7 @@ func (f *Factory) Get(gvr, fqn string, wait bool, sel labels.Selector) (runtime. func (f *Factory) waitForCacheSync(ns string) { if client.IsClusterWide(ns) { - ns = client.AllNamespaces + ns = client.BlankNamespace } f.mx.RLock() @@ -179,14 +184,14 @@ func (f *Factory) SetActiveNS(ns string) error { func (f *Factory) isClusterWide() bool { f.mx.RLock() defer f.mx.RUnlock() - _, ok := f.factories[client.AllNamespaces] + _, ok := f.factories[client.BlankNamespace] return ok } // CanForResource return an informer is user has access. func (f *Factory) CanForResource(ns, gvr string, verbs []string) (informers.GenericInformer, error) { - auth, err := f.Client().CanI(ns, gvr, verbs) + auth, err := f.Client().CanI(ns, gvr, "", verbs) if err != nil { return nil, err } @@ -218,7 +223,7 @@ func (f *Factory) ForResource(ns, gvr string) (informers.GenericInformer, error) func (f *Factory) ensureFactory(ns string) (di.DynamicSharedInformerFactory, error) { if client.IsClusterWide(ns) { - ns = client.AllNamespaces + ns = client.BlankNamespace } f.mx.Lock() defer f.mx.Unlock() @@ -272,8 +277,8 @@ func (f *Factory) ForwarderFor(path string) (Forwarder, bool) { return fwd, ok } -// BOZO!! Review!!! // ValidatePortForwards check if pods are still around for portforwards. +// BOZO!! Review!!! func (f *Factory) ValidatePortForwards() { for k, fwd := range f.forwarders { tokens := strings.Split(k, ":") @@ -285,10 +290,19 @@ func (f *Factory) ValidatePortForwards() { if len(paths) < 1 { log.Error().Msgf("Invalid path %q", tokens[0]) } - _, err := f.Get("v1/pods", paths[0], false, labels.Everything()) + o, err := f.Get("v1/pods", paths[0], false, labels.Everything()) if err != nil { fwd.Stop() delete(f.forwarders, k) + continue + } + var pod v1.Pod + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(o.(*unstructured.Unstructured).Object, &pod); err != nil { + continue + } + if pod.GetCreationTimestamp().Time.Unix() > fwd.Age().Unix() { + fwd.Stop() + delete(f.forwarders, k) } } } diff --git a/internal/watch/forwarders.go b/internal/watch/forwarders.go index 453df6054b..8eab5c0657 100644 --- a/internal/watch/forwarders.go +++ b/internal/watch/forwarders.go @@ -1,7 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package watch import ( "strings" + "time" "github.com/derailed/k9s/internal/port" "github.com/rs/zerolog/log" @@ -35,7 +39,7 @@ type Forwarder interface { SetActive(bool) // Age returns forwarder age. - Age() string + Age() time.Time // HasPortMapping returns true if port mapping exists. HasPortMapping(string) bool diff --git a/internal/watch/forwarders_test.go b/internal/watch/forwarders_test.go index aa061ba066..19e847853b 100644 --- a/internal/watch/forwarders_test.go +++ b/internal/watch/forwarders_test.go @@ -1,7 +1,11 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package watch_test import ( "testing" + "time" "github.com/derailed/k9s/internal/port" "github.com/derailed/k9s/internal/watch" @@ -179,5 +183,5 @@ func (m noOpForwarder) Port() string { return "" } func (m noOpForwarder) FQN() string { return "" } func (m noOpForwarder) Active() bool { return false } func (m noOpForwarder) SetActive(bool) {} -func (m noOpForwarder) Age() string { return "" } +func (m noOpForwarder) Age() time.Time { return time.Now() } func (m noOpForwarder) HasPortMapping(string) bool { return false } diff --git a/internal/watch/helper.go b/internal/watch/helper.go index d75169a079..2381f4b425 100644 --- a/internal/watch/helper.go +++ b/internal/watch/helper.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package watch import ( diff --git a/internal/xray/container.go b/internal/xray/container.go index 67b8e013e2..50a608607c 100644 --- a/internal/xray/container.go +++ b/internal/xray/container.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray import ( @@ -20,7 +23,7 @@ type Container struct{} func (c *Container) Render(ctx context.Context, ns string, o interface{}) error { co, ok := o.(render.ContainerRes) if !ok { - return fmt.Errorf("Expected ContainerRes, but got %T", o) + return fmt.Errorf("expected ContainerRes, but got %T", o) } f, ok := ctx.Value(internal.KeyFactory).(dao.Factory) diff --git a/internal/xray/container_test.go b/internal/xray/container_test.go index d635525aeb..09d51b7345 100644 --- a/internal/xray/container_test.go +++ b/internal/xray/container_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray_test import ( diff --git a/internal/xray/dp.go b/internal/xray/dp.go index f650f68349..a32bbafd6f 100644 --- a/internal/xray/dp.go +++ b/internal/xray/dp.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray import ( @@ -22,7 +25,7 @@ type Deployment struct{} func (d *Deployment) Render(ctx context.Context, ns string, o interface{}) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected Unstructured, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } var dp appsv1.Deployment err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &dp) diff --git a/internal/xray/dp_test.go b/internal/xray/dp_test.go index c394a92a0a..7286e9dd7f 100644 --- a/internal/xray/dp_test.go +++ b/internal/xray/dp_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray_test import ( diff --git a/internal/xray/ds.go b/internal/xray/ds.go index 3ce8e70713..d2ca86d65b 100644 --- a/internal/xray/ds.go +++ b/internal/xray/ds.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray import ( @@ -18,7 +21,7 @@ type DaemonSet struct{} func (d *DaemonSet) Render(ctx context.Context, ns string, o interface{}) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected Unstructured, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } var ds appsv1.DaemonSet err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &ds) diff --git a/internal/xray/ds_test.go b/internal/xray/ds_test.go index 4c28cca2e0..40c4db113b 100644 --- a/internal/xray/ds_test.go +++ b/internal/xray/ds_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray_test import ( diff --git a/internal/xray/generic.go b/internal/xray/generic.go index af73301964..8d30badc1d 100644 --- a/internal/xray/generic.go +++ b/internal/xray/generic.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray import ( @@ -5,22 +8,22 @@ import ( "fmt" "github.com/derailed/k9s/internal/client" - metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // Generic renders a generic resource to screen. type Generic struct { - table *metav1beta1.Table + table *metav1.Table } // SetTable sets the tabular resource. -func (g *Generic) SetTable(_ string, t *metav1beta1.Table) { +func (g *Generic) SetTable(_ string, t *metav1.Table) { g.table = t } // Render renders a K8s resource to screen. func (g *Generic) Render(ctx context.Context, ns string, o interface{}) error { - row, ok := o.(metav1beta1.TableRow) + row, ok := o.(metav1.TableRow) if !ok { return fmt.Errorf("expecting a TableRow but got %T", o) } diff --git a/internal/xray/generic_test.go b/internal/xray/generic_test.go index 24175b9a0d..61b1dc8910 100644 --- a/internal/xray/generic_test.go +++ b/internal/xray/generic_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray_test import ( diff --git a/internal/xray/ns.go b/internal/xray/ns.go index 006e78c744..a39f141af0 100644 --- a/internal/xray/ns.go +++ b/internal/xray/ns.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray import ( @@ -17,7 +20,7 @@ type Namespace struct{} func (n *Namespace) Render(ctx context.Context, ns string, o interface{}) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected NamespaceWithMetrics, but got %T", o) + return fmt.Errorf("expected NamespaceWithMetrics, but got %T", o) } var nss v1.Namespace diff --git a/internal/xray/ns_test.go b/internal/xray/ns_test.go index c8fac72abc..ab214e9250 100644 --- a/internal/xray/ns_test.go +++ b/internal/xray/ns_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray_test import ( diff --git a/internal/xray/pod.go b/internal/xray/pod.go index 1e3daadb70..dbcbf92008 100644 --- a/internal/xray/pod.go +++ b/internal/xray/pod.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray import ( @@ -21,7 +24,7 @@ type Pod struct{} func (p *Pod) Render(ctx context.Context, ns string, o interface{}) error { pwm, ok := o.(*render.PodWithMetrics) if !ok { - return fmt.Errorf("Expected PodWithMetrics, but got %T", o) + return fmt.Errorf("expected PodWithMetrics, but got %T", o) } var po v1.Pod @@ -62,7 +65,6 @@ func (p *Pod) Render(ctx context.Context, ns string, o interface{}) error { func (p *Pod) validate(node *TreeNode, po v1.Pod) error { var re render.Pod - phase := re.Phase(&po) ss := po.Status.ContainerStatuses cr, _, _ := re.Statuses(ss) diff --git a/internal/xray/pod_test.go b/internal/xray/pod_test.go index 7d14430489..23dd18bc89 100644 --- a/internal/xray/pod_test.go +++ b/internal/xray/pod_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray_test import ( diff --git a/internal/xray/rs.go b/internal/xray/rs.go index f920b3fea0..80c64f5b2d 100644 --- a/internal/xray/rs.go +++ b/internal/xray/rs.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray import ( @@ -18,7 +21,7 @@ type ReplicaSet struct{} func (r *ReplicaSet) Render(ctx context.Context, ns string, o interface{}) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected Unstructured, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } var rs appsv1.ReplicaSet err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &rs) diff --git a/internal/xray/rs_test.go b/internal/xray/rs_test.go index fab86f43dc..52e739f143 100644 --- a/internal/xray/rs_test.go +++ b/internal/xray/rs_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray_test import ( diff --git a/internal/xray/sa.go b/internal/xray/sa.go index abc1359320..a39ae06285 100644 --- a/internal/xray/sa.go +++ b/internal/xray/sa.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray import ( diff --git a/internal/xray/sa_test.go b/internal/xray/sa_test.go index 0cfadf2013..7afdf0deeb 100644 --- a/internal/xray/sa_test.go +++ b/internal/xray/sa_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray_test import ( diff --git a/internal/xray/section.go b/internal/xray/section.go index 4cd377eb68..7e171004ae 100644 --- a/internal/xray/section.go +++ b/internal/xray/section.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray import ( @@ -18,7 +21,7 @@ type Section struct { func (s *Section) Render(ctx context.Context, ns string, o interface{}) error { section, ok := o.(render.Section) if !ok { - return fmt.Errorf("Expected Section, but got %T", o) + return fmt.Errorf("expected Section, but got %T", o) } root := NewTreeNode(section.GVR, section.Title) parent, ok := ctx.Value(KeyParent).(*TreeNode) diff --git a/internal/xray/sts.go b/internal/xray/sts.go index 81ecf9c06f..819a2b5a21 100644 --- a/internal/xray/sts.go +++ b/internal/xray/sts.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray import ( @@ -18,7 +21,7 @@ type StatefulSet struct{} func (s *StatefulSet) Render(ctx context.Context, ns string, o interface{}) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected Unstructured, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } var sts appsv1.StatefulSet err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, &sts) diff --git a/internal/xray/sts_test.go b/internal/xray/sts_test.go index c939653a83..7d0444718e 100644 --- a/internal/xray/sts_test.go +++ b/internal/xray/sts_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray_test import ( diff --git a/internal/xray/svc.go b/internal/xray/svc.go index d8bac3f92a..9c0df61746 100644 --- a/internal/xray/svc.go +++ b/internal/xray/svc.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray import ( @@ -22,7 +25,7 @@ type Service struct{} func (s *Service) Render(ctx context.Context, ns string, o interface{}) error { raw, ok := o.(*unstructured.Unstructured) if !ok { - return fmt.Errorf("Expected Unstructured, but got %T", o) + return fmt.Errorf("expected Unstructured, but got %T", o) } var svc v1.Service diff --git a/internal/xray/svc_test.go b/internal/xray/svc_test.go index 8c6f28f31f..14be39e5eb 100644 --- a/internal/xray/svc_test.go +++ b/internal/xray/svc_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray_test import ( diff --git a/internal/xray/tree_node.go b/internal/xray/tree_node.go index 9a962300f2..7a62e48e79 100644 --- a/internal/xray/tree_node.go +++ b/internal/xray/tree_node.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray import ( @@ -486,7 +489,7 @@ func toEmoji(gvr string) string { return "👨🏻‍" case "networking.k8s.io/v1/networkpolicies": return "📕" - case "policy/v1beta1/poddisruptionbudgets": + case "policy/v1/poddisruptionbudgets": return "🏷 " case "policy/v1beta1/podsecuritypolicies": return "👮‍♂️" diff --git a/internal/xray/tree_node_test.go b/internal/xray/tree_node_test.go index fbce2e10c7..cd3442c7fb 100644 --- a/internal/xray/tree_node_test.go +++ b/internal/xray/tree_node_test.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package xray_test import ( diff --git a/main.go b/main.go index dd4d99ad38..05ffae543a 100644 --- a/main.go +++ b/main.go @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Authors of K9s + package main import ( diff --git a/plugins/README.md b/plugins/README.md index f49134e2e4..4afe2cf166 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -4,16 +4,17 @@ K9s plugins extend the tool to provide additional functionality via actions to f Following is an example of some of plugin files in this directory. Other files are not listed in this table. -| Plugin-Name | Description | Available on Views | Shortcut | Kubectl plugin, external dependencies | -|--------------------|------------------------------------------------------------------|--------------------|----------|---------------------------------------------------------------------------------------| -| debug-container.yml| Add [ephemeral debug container][1]
([nicolaka/netshoot][2]) | containers | Shift-d | | -| dive.yml | Dive image layers | containers | d | [Dive](https://github.com/wagoodman/dive) | -| get-all.yml | get all resources in a namespace | all | g | [Krew](https://krew.sigs.k8s.io/), [ketall](https://github.com/corneliusweig/ketall/) | -| job_suspend.yml | Suspends a running cronjob | cronjobs | Ctrl-s | | -| k3d_root_shell.yml | Root shell to k3d container | containers | Shift-s | [jq](https://stedolan.github.io/jq/) | -| log_stern.yml | View resource logs using stern | pods | Ctrl-l | | -| log_jq.yml | View resource logs using jq | pods | Ctrl-j | kubectl-plugins/kubectl-jq | -| log_full.yml | get full logs from pod/container | pods/containers | Ctrl-l | | +| Plugin-Name | Description | Available on Views | Shortcut | Kubectl plugin, external dependencies | +|--------------------|------------------------------------------------------------------|--------------------------|----------|---------------------------------------------------------------------------------------| +| debug-container.yml| Add [ephemeral debug container][1]
([nicolaka/netshoot][2]) | containers | Shift-d | | +| dive.yml | Dive image layers | containers | d | [Dive](https://github.com/wagoodman/dive) | +| get-all.yml | get all resources in a namespace | all | g | [Krew](https://krew.sigs.k8s.io/), [ketall](https://github.com/corneliusweig/ketall/) | +| job_suspend.yml | Suspends a running cronjob | cronjobs | Ctrl-s | | +| k3d_root_shell.yml | Root shell to k3d container | containers | Shift-s | [jq](https://stedolan.github.io/jq/) | +| resource-recommendations.yml | View recommendations for CPU/Memory requests based on historical data | deployments/daemonsets/statefulsets | Shift-k | [Robusta KRR](https://github.com/robusta-dev/krr) | +| log_stern.yml | View resource logs using stern | pods | Ctrl-l | | +| log_jq.yml | View resource logs using jq | pods | Ctrl-j | kubectl-plugins/kubectl-jq | +| log_full.yml | get full logs from pod/container | pods/containers | Ctrl-l | | [1]: https://kubernetes.io/docs/tasks/debug/debug-application/debug-running-pod/#ephemeral-container [2]: https://github.com/nicolaka/netshoot diff --git a/plugins/argo-rollouts.yaml b/plugins/argo-rollouts.yaml new file mode 100644 index 0000000000..b6e7fec8f5 --- /dev/null +++ b/plugins/argo-rollouts.yaml @@ -0,0 +1,51 @@ +# Manage argo-rollouts +# See https://argoproj.github.io/argo-rollouts/ +# Get rollout details +# Watch rollout progress +#

(with confirmation) Promote rollout +# (with confirmation) Restart rollout +plugins: + argo-rollouts-get: + shortCut: g + confirm: false + description: Get details + scopes: + - rollouts + command: bash + background: false + args: + - -c + - kubectl argo rollouts get rollout $NAME --context $CONTEXT -n $NAMESPACE |& less + argo-rollouts-watch: + shortCut: w + confirm: false + description: Watch progress + scopes: + - rollouts + command: bash + background: false + args: + - -c + - kubectl argo rollouts get rollout $NAME --context $CONTEXT -n $NAMESPACE -w |& less + argo-rollouts-promote: + shortCut: p + confirm: true + description: Promote + scopes: + - rollouts + command: bash + background: false + args: + - -c + - kubectl argo rollouts promote $NAME --context $CONTEXT -n $NAMESPACE |& less + argo-rollouts-restart: + shortCut: r + confirm: true + description: Restart + scopes: + - rollouts + command: bash + background: false + args: + - -c + - kubectl argo rollouts restart $NAME --context $CONTEXT -n $NAMESPACE |& less diff --git a/plugins/blame.yaml b/plugins/blame.yaml new file mode 100644 index 0000000000..97220d2935 --- /dev/null +++ b/plugins/blame.yaml @@ -0,0 +1,18 @@ +plugins: + # kubectl-blame by knight42 + # Annotate each line in the given resource's YAML with information from the managedFields to show who last modified the field. + # Source: https://github.com/knight42/kubectl-blame + # Install via: + # krew: `kubectl krew install blame` + # go: `go install github.com/knight42/kubectl-blame@latest` + blame: + shortCut: b + confirm: false + description: "Blame" + scopes: + - all + command: sh + background: false + args: + - -c + - "kubectl-blame $RESOURCE_NAME $NAME -n $NAMESPACE --context $CONTEXT | less" diff --git a/plugins/carvel.yml b/plugins/carvel.yaml similarity index 84% rename from plugins/carvel.yml rename to plugins/carvel.yaml index 02c6478ee9..dd8a4d9e90 100644 --- a/plugins/carvel.yml +++ b/plugins/carvel.yaml @@ -1,5 +1,5 @@ # $HOME/.k9s/plugin.yml -plugin: +plugins: kapp-inspect: shortCut: Shift-Z confirm: false @@ -10,7 +10,7 @@ plugin: background: false args: - -c - - "export FORCE_COLOR=1;kapp inspect -a $NAME.app --namespace $NAMESPACE --kubeconfig-context $CONTEXT --color --tty |& less -R" + - "export FORCE_COLOR=1;kapp inspect -a $NAME.app --namespace $NAMESPACE --kubeconfig-context $CONTEXT --color --tty | less -RK" kctrl-app-status: shortCut: Shift-Q confirm: false @@ -21,7 +21,7 @@ plugin: background: false args: - -c - - "export FORCE_COLOR=1;kctrl app status -a $NAME --namespace $NAMESPACE --kubeconfig-context $CONTEXT --color --tty |& less -R" + - "export FORCE_COLOR=1;kctrl app status -a $NAME --namespace $NAMESPACE --kubeconfig-context $CONTEXT --color --tty | less -RK" kctrl-app-pause: shortCut: Shift-T confirm: false @@ -32,9 +32,9 @@ plugin: background: false args: - -c - - "export FORCE_COLOR=1;kctrl app pause -a $NAME --namespace $NAMESPACE --kubeconfig-context $CONTEXT --yes --color --tty |& less -R" + - "export FORCE_COLOR=1;kctrl app pause -a $NAME --namespace $NAMESPACE --kubeconfig-context $CONTEXT --yes --color --tty | less -RK" kctrl-app-kick: - shortCut: Shift-Z + shortCut: Shift-K confirm: false description: kctrl app kick scopes: @@ -43,5 +43,4 @@ plugin: background: false args: - -c - - "export FORCE_COLOR=1;kctrl app kick -a $NAME --namespace $NAMESPACE --kubeconfig-context $CONTEXT --yes --color --tty |& less -R" - + - "export FORCE_COLOR=1;kctrl app kick -a $NAME --namespace $NAMESPACE --kubeconfig-context $CONTEXT --yes --color --tty | less -RK" diff --git a/plugins/cert-manager.yaml b/plugins/cert-manager.yaml new file mode 100644 index 0000000000..9c3f1aff41 --- /dev/null +++ b/plugins/cert-manager.yaml @@ -0,0 +1,36 @@ +# Manage cert-manager Certificate resouces via cmctl. +# See: https://github.com/cert-manager/cmctl +plugins: + cert-status: + shortCut: Shift-S + confirm: false + description: Certificate status + scopes: + - certificates + command: bash + background: false + args: + - -c + - "cmctl status certificate --context $CONTEXT -n $NAMESPACE $NAME |& less" + cert-renew: + shortCut: Shift-R + confirm: false + description: Certificate renew + scopes: + - certificates + command: bash + background: false + args: + - -c + - "cmctl renew --context $CONTEXT -n $NAMESPACE $NAME |& less" + secret-inspect: + shortCut: Shift-I + confirm: false + description: Inspect secret + scopes: + - secrets + command: bash + background: false + args: + - -c + - "cmctl inspect secret --context $CONTEXT -n $NAMESPACE $NAME |& less" \ No newline at end of file diff --git a/plugins/crossplane.yaml b/plugins/crossplane.yaml new file mode 100644 index 0000000000..14d7915e3f --- /dev/null +++ b/plugins/crossplane.yaml @@ -0,0 +1,21 @@ +plugins: + # List all the resources managed by a Composite Resource + kube-lineage: + shortCut: Ctrl-X + confirm: false + description: "Kube Lineage" + scopes: + - all + command: sh + background: false + args: + - -c + - >- + kubectl lineage + -d 6 + --exclude-types Event,ProviderConfigUsage.aws.upbound.io,ProviderConfigUsage.kubernetes.crossplane.io + --show-group + --context $CONTEXT + $RESOURCE_NAME + $NAME + | less -K diff --git a/plugins/crossplane.yml b/plugins/crossplane.yml deleted file mode 100644 index 8b16340dce..0000000000 --- a/plugins/crossplane.yml +++ /dev/null @@ -1,13 +0,0 @@ -plugin: - # List all the resources managed by a Composite Resource - kube-lineage: - shortCut: Ctrl-X - confirm: false - description: "Kube Lineage" - scopes: - - all - command: sh - background: false - args: - - -c - - "kubectl lineage -d 6 --exclude-types Event,ProviderConfigUsage.aws.upbound.io,ProviderConfigUsage.kubernetes.crossplane.io --show-group --context $CONTEXT $RESOURCE_NAME $NAME | less" \ No newline at end of file diff --git a/plugins/debug-container.yml b/plugins/debug-container.yaml similarity index 67% rename from plugins/debug-container.yml rename to plugins/debug-container.yaml index 2040cefcf3..aefba8801c 100644 --- a/plugins/debug-container.yml +++ b/plugins/debug-container.yaml @@ -1,9 +1,10 @@ -plugin: +plugins: #--- Create debug container for selected pod in current namespace # See https://kubernetes.io/docs/tasks/debug/debug-application/debug-running-pod/#ephemeral-container debug: shortCut: Shift-D description: Add debug container + dangerous: true scopes: - containers command: bash @@ -11,4 +12,4 @@ plugin: confirm: true args: - -c - - "kubectl debug -it -n=$NAMESPACE $POD --target=$NAME --image=nicolaka/netshoot:v0.11 --share-processes -- bash" \ No newline at end of file + - "kubectl debug -it --context $CONTEXT -n=$NAMESPACE $POD --target=$NAME --image=nicolaka/netshoot:v0.12 --share-processes -- bash" \ No newline at end of file diff --git a/plugins/dive.yml b/plugins/dive.yaml similarity index 95% rename from plugins/dive.yml rename to plugins/dive.yaml index c92205ac32..090bc2c680 100644 --- a/plugins/dive.yml +++ b/plugins/dive.yaml @@ -1,4 +1,4 @@ -plugin: +plugins: dive: shortCut: d confirm: false diff --git a/plugins/flux.yaml b/plugins/flux.yaml new file mode 100644 index 0000000000..72a9655542 --- /dev/null +++ b/plugins/flux.yaml @@ -0,0 +1,207 @@ +# $HOME/.k9s/plugin.yml +# move selected line to chosen resource in K9s, then: +# Shift-T (with confirmation) to toggle helm releases or kustomizations suspend and resume +# Shift-R (no confirmation) to reconcile a git source or a helm release or a kustomization +plugins: + toggle-helmrelease: + shortCut: Shift-T + confirm: true + scopes: + - helmreleases + description: Toggle to suspend or resume a HelmRelease + command: bash + background: false + args: + - -c + - >- + suspended=$(kubectl --context $CONTEXT get helmreleases -n $NAMESPACE $NAME -o=custom-columns=TYPE:.spec.suspend | tail -1); + verb=$([ $suspended = "true" ] && echo "resume" || echo "suspend"); + flux + $verb helmrelease + --context $CONTEXT + -n $NAMESPACE $NAME + | less -K + toggle-kustomization: + shortCut: Shift-T + confirm: true + scopes: + - kustomizations + description: Toggle to suspend or resume a Kustomization + command: bash + background: false + args: + - -c + - >- + suspended=$(kubectl --context $CONTEXT get kustomizations -n $NAMESPACE $NAME -o=custom-columns=TYPE:.spec.suspend | tail -1); + verb=$([ $suspended = "true" ] && echo "resume" || echo "suspend"); + flux + $verb kustomization + --context $CONTEXT + -n $NAMESPACE $NAME + | less -K + reconcile-git: + shortCut: Shift-R + confirm: false + description: Flux reconcile + scopes: + - gitrepositories + command: bash + background: false + args: + - -c + - >- + flux + reconcile source git + --context $CONTEXT + -n $NAMESPACE $NAME + | less -K + reconcile-hr: + shortCut: Shift-R + confirm: false + description: Flux reconcile + scopes: + - helmreleases + command: bash + background: false + args: + - -c + - >- + flux + reconcile helmrelease + --context $CONTEXT + -n $NAMESPACE $NAME + | less -K + reconcile-helm-repo: + shortCut: Shift-Z + description: Flux reconcile + scopes: + - helmrepositories + command: bash + background: false + confirm: false + args: + - -c + - >- + flux + reconcile source helm + --context $CONTEXT + -n $NAMESPACE $NAME + | less -K + reconcile-oci-repo: + shortCut: Shift-Z + description: Flux reconcile + scopes: + - ocirepositories + command: bash + background: false + confirm: false + args: + - -c + - >- + flux + reconcile source oci + --context $CONTEXT + -n $NAMESPACE $NAME + | less -K + reconcile-ks: + shortCut: Shift-R + confirm: false + description: Flux reconcile + scopes: + - kustomizations + command: bash + background: false + args: + - -c + - >- + flux + reconcile kustomization + --context $CONTEXT + -n $NAMESPACE $NAME + | less -K + reconcile-ir: + shortCut: Shift-R + confirm: false + description: Flux reconcile + scopes: + - imagerepositories + command: sh + background: false + args: + - -c + - >- + flux + reconcile image repository + --context $CONTEXT + -n $NAMESPACE $NAME + | less -K + reconcile-iua: + shortCut: Shift-R + confirm: false + description: Flux reconcile + scopes: + - imageupdateautomations + command: sh + background: false + args: + - -c + - >- + flux + reconcile image update + --context $CONTEXT + -n $NAMESPACE $NAME + | less -K + trace: + shortCut: Shift-A + confirm: false + description: Flux trace + scopes: + - all + command: bash + background: false + args: + - -c + - >- + resource=$(echo $RESOURCE_NAME | sed -E 's/ies$/y/' | sed -E 's/ses$/se/' | sed -E 's/(s|es)$//g') + flux + trace + --context $CONTEXT + --kind $resource + --api-version $RESOURCE_GROUP/$RESOURCE_VERSION + --namespace $NAMESPACE $NAME + | less -K + # credits: https://github.com/fluxcd/flux2/discussions/2494 + get-suspended-helmreleases: + shortCut: Shift-S + confirm: false + description: Suspended Helm Releases + scopes: + - helmrelease + command: sh + background: false + args: + - -c + - >- + kubectl get + --context $CONTEXT + --all-namespaces + helmreleases.helm.toolkit.fluxcd.io -o json + | jq -r '.items[] | select(.spec.suspend==true) | [.metadata.namespace,.metadata.name,.spec.suspend] | @tsv' + | less -K + get-suspended-kustomizations: + shortCut: Shift-S + confirm: false + description: Suspended Kustomizations + scopes: + - kustomizations + command: sh + background: false + args: + - -c + - >- + kubectl get + --context $CONTEXT + --all-namespaces + kustomizations.kustomize.toolkit.fluxcd.io -o json + | jq -r '.items[] | select(.spec.suspend==true) | [.metadata.name,.spec.suspend] | @tsv' + | less -K diff --git a/plugins/flux.yml b/plugins/flux.yml deleted file mode 100644 index d997e387f6..0000000000 --- a/plugins/flux.yml +++ /dev/null @@ -1,93 +0,0 @@ -# $HOME/.k9s/plugin.yml -# move selected line to chosen resource in K9s, then: -# Shift-T (with confirmation) to toggle helm releases or kustomizations suspend and resume -# Shift-R (no confirmation) to reconcile a git source or a helm release or a kustomization -plugin: - toggle-helmrelease: - shortCut: Shift-T - confirm: true - scopes: - - helmreleases - description: Toggle to suspend or resume a HelmRelease - command: bash - background: false - args: - - -c - - "flux --context $CONTEXT $([ $(kubectl --context $CONTEXT get helmreleases -n $NAMESPACE $NAME -o=custom-columns=TYPE:.spec.suspend | tail -1) = \"true\" ] && echo \"resume\" || echo \"suspend\") helmrelease -n $NAMESPACE $NAME |& less" - toggle-kustomization: - shortCut: Shift-T - confirm: true - scopes: - - kustomizations - description: Toggle to suspend or resume a Kustomization - command: bash - background: false - args: - - -c - - "flux --context $CONTEXT $([ $(kubectl --context $CONTEXT get kustomizations -n $NAMESPACE $NAME -o=custom-columns=TYPE:.spec.suspend | tail -1) = \"true\" ] && echo \"resume\" || echo \"suspend\") kustomization -n $NAMESPACE $NAME |& less" - reconcile-git: - shortCut: Shift-R - confirm: false - description: Flux reconcile - scopes: - - gitrepositories - command: bash - background: false - args: - - -c - - "flux --context $CONTEXT reconcile source git -n $NAMESPACE $NAME |& less" - reconcile-hr: - shortCut: Shift-R - confirm: false - description: Flux reconcile - scopes: - - helmreleases - command: bash - background: false - args: - - -c - - "flux --context $CONTEXT reconcile helmrelease -n $NAMESPACE $NAME |& less" - reconcile-helm-repo: - shortCut: Shift-Z - description: Flux reconcile - scopes: - - helmrepositories - command: bash - background: false - confirm: false - args: - - -c - - "flux reconcile source helm --context $CONTEXT -n $NAMESPACE $NAME |& less" - reconcile-oci-repo: - shortCut: Shift-Z - description: Flux reconcile - scopes: - - ocirepositories - command: bash - background: false - confirm: false - args: - - -c - - "flux reconcile source oci --context $CONTEXT -n $NAMESPACE $NAME |& less" - reconcile-ks: - shortCut: Shift-R - confirm: false - description: Flux reconcile - scopes: - - kustomizations - command: bash - background: false - args: - - -c - - "flux --context $CONTEXT reconcile kustomization -n $NAMESPACE $NAME |& less" - trace: - shortCut: Shift-A - confirm: false - description: Flux trace - scopes: - - all - command: bash - background: false - args: - - -c - - "flux --context $CONTEXT trace --kind `echo $RESOURCE_NAME | sed -E 's/ies$/y/' | sed -E 's/ses$/se/' | sed -E 's/(s|es)$//g'` --api-version $RESOURCE_GROUP/$RESOURCE_VERSION --namespace $NAMESPACE $NAME |& less" diff --git a/plugins/get-all.yml b/plugins/get-all.yaml similarity index 63% rename from plugins/get-all.yml rename to plugins/get-all.yaml index 1e1d871945..58f65d71c3 100644 --- a/plugins/get-all.yml +++ b/plugins/get-all.yaml @@ -1,24 +1,24 @@ -plugin: +plugins: #get all resources in a namespace using the krew get-all plugin get-all-namespace: shortCut: g confirm: false description: get-all scopes: - - namespaces + - namespaces command: sh background: false args: - - -c - - "kubectl get-all --context $CONTEXT -n $NAME | less" + - -c + - "kubectl get-all --context $CONTEXT -n $NAME | less -K" get-all-other: shortCut: g confirm: false description: get-all scopes: - - all + - all command: sh background: false args: - - -c - - "kubectl get-all --context $CONTEXT -n $NAMESPACE | less" + - -c + - "kubectl get-all --context $CONTEXT -n $NAMESPACE | less -K" diff --git a/plugins/get_suspended.yml b/plugins/get_suspended.yml deleted file mode 100644 index a5270a27b9..0000000000 --- a/plugins/get_suspended.yml +++ /dev/null @@ -1,23 +0,0 @@ -# credits: https://github.com/fluxcd/flux2/discussions/2494 - get-suspended-helmreleases: - shortCut: Shift-S - confirm: false - description: Suspended Helm Releases - scopes: - - helmrelease - command: sh - background: false - args: - - -c - - "kubectl get --all-namespaces helmreleases.helm.toolkit.fluxcd.io -o json | jq -r '.items[] | select(.spec.suspend==true) | [.metadata.namespace,.metadata.name,.spec.suspend] | @tsv' | less" - get-suspended-kustomizations: - shortCut: Shift-S - confirm: false - description: Suspended Kustomizations - scopes: - - kustomizations - command: sh - background: false - args: - - -c - - "kubectl get --all-namespaces kustomizations.kustomize.toolkit.fluxcd.io -o json | jq -r '.items[] | select(.spec.suspend==true) | [.metadata.name,.spec.suspend] | @tsv' | less" diff --git a/plugins/helm-default-values.yaml b/plugins/helm-default-values.yaml new file mode 100644 index 0000000000..253d03d689 --- /dev/null +++ b/plugins/helm-default-values.yaml @@ -0,0 +1,25 @@ +plugins: + helm-default-values: + shortCut: Shift-V + confirm: false + description: Chart Default Values + scopes: + - helm + command: sh + background: false + args: + - -c + - >- + revision=$(helm history -n $NAMESPACE --kube-context $CONTEXT $COL-NAME | grep deployed | cut -d$'\t' -f1 | tr -d ' \t'); + kubectl + get secrets + --context $CONTEXT + -n $NAMESPACE + sh.helm.release.v1.$COL-NAME.v$revision -o yaml + | yq e '.data.release' - + | base64 -d + | base64 -d + | gunzip + | jq -r '.chart.values' + | yq -P + | less -K diff --git a/plugins/helm-default-values.yml b/plugins/helm-default-values.yml deleted file mode 100644 index b79705d598..0000000000 --- a/plugins/helm-default-values.yml +++ /dev/null @@ -1,13 +0,0 @@ -plugin: - helm-default-values: - shortCut: Shift-V - confirm: false - description: Chart Default Values - scopes: - - helm - command: sh - background: false - args: - - -c - - "kubectl get secrets --context $CONTEXT -n $NAMESPACE sh.helm.release.v1.$COL-NAME.v$(helm history -n $NAMESPACE --kube-context $CONTEXT $COL-NAME | grep deployed | cut -d$'\\t' -f1 | tr -d ' \\t') -o yaml | yq e '.data.release' - | base64 -d | base64 -d | gunzip | jq -r '.chart.values' | yq -P | less" - diff --git a/plugins/helm-purge.yml b/plugins/helm-purge.yaml similarity index 90% rename from plugins/helm-purge.yml rename to plugins/helm-purge.yaml index eb106c07ef..c84a8eaa30 100644 --- a/plugins/helm-purge.yml +++ b/plugins/helm-purge.yaml @@ -1,9 +1,10 @@ # $HOME/.k9s/plugin.yml -plugin: +plugins: # Issues a helm delete --purge for the resource associated with the selected pod helm-purge: shortCut: Ctrl-P description: Helm Purge + dangerous: true scopes: - po command: kubectl diff --git a/plugins/helm_values.yml b/plugins/helm-values.yaml similarity index 62% rename from plugins/helm_values.yml rename to plugins/helm-values.yaml index 36d8fdb2eb..97e70cf850 100644 --- a/plugins/helm_values.yml +++ b/plugins/helm-values.yaml @@ -1,14 +1,14 @@ # View user-supplied values when the helm chart was created -plugin: +plugins: helm-values: shortCut: v confirm: false description: Values scopes: - - helm + - helm command: sh background: false args: - - -c - - "helm get values $COL-NAME -n $NAMESPACE --kube-context $CONTEXT | less" + - -c + - "helm get values $COL-NAME -n $NAMESPACE --kube-context $CONTEXT | less -K" diff --git a/plugins/job_suspend.yml b/plugins/job-suspend.yaml similarity index 93% rename from plugins/job_suspend.yml rename to plugins/job-suspend.yaml index d674dd2d2c..799f884efd 100644 --- a/plugins/job_suspend.yml +++ b/plugins/job-suspend.yaml @@ -1,8 +1,9 @@ -plugin: +plugins: # Suspends/Resumes a cronjob toggleCronjob: shortCut: Ctrl-S confirm: true + dangerous: true scopes: - cj description: Toggle to suspend or resume a running cronjob diff --git a/plugins/k3d_root_shell.yml b/plugins/k3d-root-shell.yaml similarity index 97% rename from plugins/k3d_root_shell.yml rename to plugins/k3d-root-shell.yaml index 295c680833..d44304284c 100644 --- a/plugins/k3d_root_shell.yml +++ b/plugins/k3d-root-shell.yaml @@ -1,8 +1,9 @@ -plugin: +plugins: # Opens a shell to k3d container as root k3d-root-shell: shortCut: Shift-S confirm: false + dangerous: true description: "Root Shell" scopes: - containers diff --git a/plugins/kubectl-plugins/kubectl-jq b/plugins/kubectl-plugins/kubectl-jq index abbd3dda66..44a8cd61fe 100755 --- a/plugins/kubectl-plugins/kubectl-jq +++ b/plugins/kubectl-plugins/kubectl-jq @@ -1,3 +1,3 @@ #!/bin/bash -/usr/local/bin/kubectl logs -f $1 -n $2 --context $3 | jq -rR '. as $raw | try (fromjson | .message) catch ("\u001b[31m" + $raw + "\u001b[0m")' \ No newline at end of file +kubectl logs -f $1 -n $2 --context $3 | jq -rR '. as $raw | try (fromjson | .message) catch ("\u001b[31m" + $raw + "\u001b[0m")' diff --git a/plugins/liveMigration.yaml b/plugins/liveMigration.yaml new file mode 100644 index 0000000000..a0fbbe72b4 --- /dev/null +++ b/plugins/liveMigration.yaml @@ -0,0 +1,35 @@ +# $XDG_CONFIG_HOME/k9s/plugins.yaml +plugins: + # liveMigration plugin config by rabin-io + # + # Trigger virtual machine live migration, for VM's running on k8s cluster using kubevirt + # or Openshift with CNV (OpenShift Virtualization) installed. + # + # Require `virtctl` cli in your PATH, + # can be downloaded from Openshift `Command Line Tools` page + # or from kubevirt site https://kubevirt.io/user-guide/operations/virtctl_client_tool/ + # + # + liveMigration: + # Can be triggered from the VMI (VirtualMachineInstance) view, with shortcut `m` + shortCut: m + # Description to show in K9s menu + description: Live Migrate moves VM to another compute node + # Enable confirmation dialog + confirm: true + dangerous: true + # Collections of views that support this shortcut. (You can use `all`) + scopes: + - virtualmachineinstance + # Whether or not to run the command in background mode + background: false + # The command to run upon invocation. + command: virtctl + # Defines the command arguments + args: + - migrate + - $NAME + - -n + - $NAMESPACE + - --context + - $CONTEXT diff --git a/plugins/log_full.yml b/plugins/log-full.yaml similarity index 99% rename from plugins/log_full.yml rename to plugins/log-full.yaml index 304a86d384..452c1dbc91 100644 --- a/plugins/log_full.yml +++ b/plugins/log-full.yaml @@ -1,4 +1,4 @@ -plugin: +plugins: # See https://k9scli.io/topics/plugins/ raw-logs-follow: shortCut: Ctrl-L diff --git a/plugins/log_jq.yml b/plugins/log-jq.yaml similarity index 97% rename from plugins/log_jq.yml rename to plugins/log-jq.yaml index f4c40e02f3..331427b92f 100644 --- a/plugins/log_jq.yml +++ b/plugins/log-jq.yaml @@ -1,4 +1,4 @@ -plugin: +plugins: # Sends logs over to jq for processing. This leverages kubectl plugin kubectl-jq. jqlogs: shortCut: Ctrl-J diff --git a/plugins/log_stern.yml b/plugins/log-stern.yaml similarity index 97% rename from plugins/log_stern.yml rename to plugins/log-stern.yaml index 9f7b9ed079..1e850b385d 100644 --- a/plugins/log_stern.yml +++ b/plugins/log-stern.yaml @@ -1,4 +1,4 @@ -plugin: +plugins: # Leverage stern (https://github.com/stern/stern) to output logs. stern: shortCut: Ctrl-L diff --git a/plugins/openssl.yaml b/plugins/openssl.yaml new file mode 100644 index 0000000000..c21bf31631 --- /dev/null +++ b/plugins/openssl.yaml @@ -0,0 +1,25 @@ +# Inspect certificate chains with openssl. +# See: https://github.com/openssl/openssl. +plugins: + secret-openssl-ca: + shortCut: Ctrl-O + confirm: false + description: Openssl ca.crt + scopes: + - secrets + command: bash + background: false + args: + - -c + - kubectl get secret --context $CONTEXT -n $NAMESPACE $NAME -o jsonpath='{.data.ca\.crt}' | base64 -d | openssl storeutl -noout -text -certs /dev/stdin |& less + secret-openssl-tls: + shortCut: Shift-O + confirm: false + description: Openssl tls.crt + scopes: + - secrets + command: bash + background: false + args: + - -c + - kubectl get secret --context $CONTEXT -n $NAMESPACE $NAME -o jsonpath='{.data.tls\.crt}' | base64 -d | openssl storeutl -noout -text -certs /dev/stdin |& less \ No newline at end of file diff --git a/plugins/remove-finalizers.yaml b/plugins/remove-finalizers.yaml new file mode 100644 index 0000000000..b7a83d67d5 --- /dev/null +++ b/plugins/remove-finalizers.yaml @@ -0,0 +1,33 @@ +# Removes all finalizers from the selected resource. Finalizers are namespaced keys that tell Kubernetes to wait +# until specific conditions are met before it fully deletes resources marked for deletion. +# Before deleting an object you need to ensure that all finalizers has been removed. Usually this would be done +# by the specific controller but under some circumstances it is possible to encounter a set of objects blocked +# for deletion. +# This plugins makes this task easier by providing a shortcut to directly removing them all. +# Be careful when using this plugin as it may leave dangling resources or instantly deleting resources that were +# blocked by the finalizers. +# Author: github.com/jalvarezit +plugins: + remove_finalizers: + shortCut: Ctrl-F + confirm: true + dangerous: true + scopes: + - all + description: | + Removes all finalizers from selected resource. Be careful when using it, + it may leave dangling resources or delete them + command: kubectl + background: true + args: + - patch + - --context + - $CONTEXT + - --namespace + - $NAMESPACE + - $RESOURCE_NAME + - $NAME + - -p + - '{"metadata":{"finalizers":null}}' + - --type + - merge diff --git a/plugins/resource-recommendations.yaml b/plugins/resource-recommendations.yaml new file mode 100644 index 0000000000..b85518f60e --- /dev/null +++ b/plugins/resource-recommendations.yaml @@ -0,0 +1,28 @@ +plugins: +# Author: Daniel Rubin +# Get recommendations for CPU/Memory requests and limits using Robusta KRR +# Requires Prometheus in the Cluster and Robusta KRR (https://github.com/robusta-dev/krr) on your system +# Open K9s in deployments/daemonsets/statefulsets view, then: +# Shift-K to get recommendations + krr: + shortCut: Shift-K + description: Get krr + scopes: + - deployments + - daemonsets + - statefulsets + command: bash + background: false + confirm: false + args: + - -c + - | + LABELS=$(kubectl get $RESOURCE_NAME $NAME -n $NAMESPACE --context $CONTEXT --show-labels | awk '{print $NF}' | awk '{if(NR>1)print}') + krr simple --cluster $CONTEXT --selector $LABELS + echo "Press 'q' to exit" + while : ; do + read -n 1 k <&1 + if [[ $k = q ]] ; then + break + fi + done \ No newline at end of file diff --git a/plugins/rm-ns.yml b/plugins/rm-ns.yaml similarity index 92% rename from plugins/rm-ns.yml rename to plugins/rm-ns.yaml index 14a2a251d8..a73592c040 100644 --- a/plugins/rm-ns.yml +++ b/plugins/rm-ns.yaml @@ -1,8 +1,9 @@ -plugin: +plugins: # remove finalizers from a stuck namespace rm-ns: shortCut: n confirm: true + dangerous: true description: Remove NS Finalizers scopes: - namespace diff --git a/plugins/schema.json b/plugins/schema.json deleted file mode 100644 index 3dd50016e9..0000000000 --- a/plugins/schema.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "Schema for k9s CLI plugin.yml file : https://k9scli.io/topics/plugins", - "type": "object", - "additionalProperties": false, - "properties": { - "plugin": { - "type": "object", - "additionalProperties": { - "type": "object", - "additionalProperties": false, - "properties": { - "shortCut": { - "description": "Define a mnemonic to invoke the plugin", - "type": "string" - }, - "description": { - "description": "What will be shown on the K9s menu", - "type": "string" - }, - "confirm": { - "description": "See the command that is going to be executed and gives you an option to confirm", - "type": "boolean" - }, - "scopes": { - "type": "array", - "description": "Collections of views that support this shortcut. (You can use `all`)", - "items": { - "type": "string" - } - }, - "command": { - "description": "The command to run upon invocation. Can use Krew plugins here too!", - "type": "string" - }, - "background": { - "description": "Whether or not to run the command in background mode", - "type": "boolean" - }, - "args": { - "description": "Defines the command arguments", - "type": "array", - "items": { - "type": "string" - } - } - }, - "required": ["shortCut", "description", "scopes", "command"] - }, - "required": [] - } - }, - "required": ["plugin"] -} diff --git a/plugins/watch_events.yml b/plugins/watch-events.yaml similarity index 97% rename from plugins/watch_events.yml rename to plugins/watch-events.yaml index ffd8b2fc59..24ef63e669 100644 --- a/plugins/watch_events.yml +++ b/plugins/watch-events.yaml @@ -1,8 +1,7 @@ # watch events on selected resources # requires linux "watch" command # change '-n' to adjust refresh time in seconds - -plugin: +plugins: watch-events: shortCut: Shift-E confirm: false diff --git a/skins/axual.yml b/skins/axual.yaml similarity index 98% rename from skins/axual.yml rename to skins/axual.yaml index bc7b8701df..816eccc2cf 100644 --- a/skins/axual.yml +++ b/skins/axual.yaml @@ -111,7 +111,7 @@ k9s: indicator: fgColor: *red bgColor: *blue - toggleOnColor: *green + toggleOnColor: *yellow toggleOffColor: *grey # Chart drawing diff --git a/skins/black_and_wtf.yml b/skins/black-and-wtf.yaml similarity index 98% rename from skins/black_and_wtf.yml rename to skins/black-and-wtf.yaml index 2b903ad78a..69fe02ef4a 100644 --- a/skins/black_and_wtf.yml +++ b/skins/black-and-wtf.yaml @@ -24,7 +24,7 @@ k9s: prompt: fgColor: *fg bgColor: *bg - suggestColor: &gray + suggestColor: *gray info: fgColor: *text sectionColor: *fg diff --git a/skins/dracula.yml b/skins/dracula.yaml similarity index 100% rename from skins/dracula.yml rename to skins/dracula.yaml diff --git a/skins/everforest-dark.yaml b/skins/everforest-dark.yaml new file mode 100644 index 0000000000..5f2919371f --- /dev/null +++ b/skins/everforest-dark.yaml @@ -0,0 +1,114 @@ +# ----------------------------------------------------------------------------- +# Everforest Dark +# https://github.com/sainnhe/everforest/blob/master/palette.md#dark +# ----------------------------------------------------------------------------- +# +text: &text "#d3c6aa" +base: &base "#1e2326" +overlay: &overlay "#2e383c" +muted: &muted "#495156" +red: &red "#e67e80" +blue: &blue "#7fbbb3" +yellow: &yellow "#dbbc7f" +green: &green "#83c092" +pink: &pink "#d699b6" +orange: &orange "#e69875" + +# Skin... +k9s: + # General K9s styles + body: + fgColor: *text + bgColor: *base + logoColor: *green + # Command prompt styles + prompt: + fgColor: *text + bgColor: *base + suggestColor: *green + border: + command: *orange + default: *blue + # ClusterInfoView styles. + info: + fgColor: *green + sectionColor: *text + # Dialog styles. + dialog: + fgColor: *text + bgColor: *base + buttonFgColor: *text + buttonBgColor: *green + buttonFocusFgColor: *yellow + buttonFocusBgColor: *green + labelFgColor: *yellow + fieldFgColor: *text + frame: + # Borders styles. + border: + fgColor: *overlay + focusColor: *overlay + menu: + fgColor: *text + keyColor: *green + # Used for favorite namespaces + numKeyColor: *green + # CrumbView attributes for history navigation. + crumbs: + fgColor: *text + bgColor: *overlay + activeColor: *overlay + # Resource status and update styles + status: + newColor: *green + modifyColor: *red + addColor: *blue + errorColor: *pink + highlightcolor: *yellow + killColor: *muted + completedColor: *muted + # Border title styles. + title: + fgColor: *text + bgColor: *overlay + highlightColor: *yellow + counterColor: *green + filterColor: *green + views: + # Charts skins... + charts: + bgColor: default + defaultDialColors: + - *green + - *pink + defaultChartColors: + - *green + - *pink + # TableView attributes. + table: + fgColor: *text + bgColor: *base + # Header row styles. + header: + fgColor: *text + bgColor: *base + sorterColor: *red + # Xray view attributes. + xray: + fgColor: *text + bgColor: *base + cursorColor: *overlay + graphicColor: *green + showIcons: false + # YAML info styles. + yaml: + keyColor: *green + colonColor: *green + valueColor: *text + # Logs styles. + logs: + fgColor: *text + bgColor: *base + indicator: + fgColor: *text + bgColor: *base diff --git a/skins/everforest-light.yaml b/skins/everforest-light.yaml new file mode 100644 index 0000000000..dceab3e2ca --- /dev/null +++ b/skins/everforest-light.yaml @@ -0,0 +1,114 @@ +# ----------------------------------------------------------------------------- +# Everforest Light +# https://github.com/sainnhe/everforest/blob/master/palette.md#dark +# ----------------------------------------------------------------------------- +# +text: &text "#5c6a72" +base: &base "#f2efdf" +overlay: &overlay "#fffbef" +muted: &muted "#edeada" +red: &red "#f85552" +blue: &blue "#3a94c5" +yellow: &yellow "#dfa000" +green: &green "#35a77c" +pink: &pink "#df69ba" +orange: &orange "#f57d26" + +# Skin... +k9s: + # General K9s styles + body: + fgColor: *text + bgColor: *base + logoColor: *green + # Command prompt styles + prompt: + fgColor: *text + bgColor: *base + suggestColor: *green + border: + command: *orange + default: *blue + # ClusterInfoView styles. + info: + fgColor: *green + sectionColor: *text + # Dialog styles. + dialog: + fgColor: *text + bgColor: *base + buttonFgColor: *text + buttonBgColor: *green + buttonFocusFgColor: *yellow + buttonFocusBgColor: *green + labelFgColor: *yellow + fieldFgColor: *text + frame: + # Borders styles. + border: + fgColor: *overlay + focusColor: *overlay + menu: + fgColor: *text + keyColor: *green + # Used for favorite namespaces + numKeyColor: *green + # CrumbView attributes for history navigation. + crumbs: + fgColor: *text + bgColor: *overlay + activeColor: *overlay + # Resource status and update styles + status: + newColor: *green + modifyColor: *red + addColor: *blue + errorColor: *pink + highlightcolor: *yellow + killColor: *muted + completedColor: *muted + # Border title styles. + title: + fgColor: *text + bgColor: *overlay + highlightColor: *yellow + counterColor: *green + filterColor: *green + views: + # Charts skins... + charts: + bgColor: default + defaultDialColors: + - *green + - *pink + defaultChartColors: + - *green + - *pink + # TableView attributes. + table: + fgColor: *text + bgColor: *base + # Header row styles. + header: + fgColor: *text + bgColor: *base + sorterColor: *red + # Xray view attributes. + xray: + fgColor: *text + bgColor: *base + cursorColor: *overlay + graphicColor: *green + showIcons: false + # YAML info styles. + yaml: + keyColor: *green + colonColor: *green + valueColor: *text + # Logs styles. + logs: + fgColor: *text + bgColor: *base + indicator: + fgColor: *text + bgColor: *base diff --git a/skins/gruvbox-dark.yml b/skins/gruvbox-dark.yaml similarity index 100% rename from skins/gruvbox-dark.yml rename to skins/gruvbox-dark.yaml diff --git a/skins/gruvbox-light.yml b/skins/gruvbox-light.yaml similarity index 100% rename from skins/gruvbox-light.yml rename to skins/gruvbox-light.yaml diff --git a/skins/in_the_navy.yml b/skins/in-the-navy.yaml similarity index 100% rename from skins/in_the_navy.yml rename to skins/in-the-navy.yaml diff --git a/skins/kanagawa.yaml b/skins/kanagawa.yaml new file mode 100644 index 0000000000..11b4452260 --- /dev/null +++ b/skins/kanagawa.yaml @@ -0,0 +1,114 @@ +# ----------------------------------------------------------------------------- +# Kanagawa Skin +# ----------------------------------------------------------------------------- + +# Styles... +foreground: &foreground "#dcd7ba" +background: &background "#1f1f28" +black: &black "#090618" +blue: &blue "#7e9cd8" +green: &green "#76946a" +grey: &grey "#727169" +orange: &orange "#ffa066" +purple: &purple "#957fb8" +red: &red "#c34043" +yellow: &yellow "#c0a36e" +yellow_bright: &yellow_bright "#e6c384" + +# Skin... +k9s: + body: + fgColor: *foreground + bgColor: *background + logoColor: *green + prompt: + fgColor: *foreground + bgColor: *background + suggestColor: *orange + info: + fgColor: *grey + sectionColor: *green + help: + fgColor: *foreground + bgColor: *background + keyColor: *yellow + numKeyColor: *blue + sectionColor: *purple + dialog: + fgColor: *black + bgColor: *background + buttonFgColor: *foreground + buttonBgColor: *green + buttonFocusFgColor: *black + buttonFocusBgColor: *blue + labelFgColor: *orange + fieldFgColor: *blue + frame: + border: + fgColor: *green + focusColor: *green + menu: + fgColor: *grey + keyColor: *yellow + numKeyColor: *yellow + crumbs: + fgColor: *black + bgColor: *green + activeColor: *yellow + status: + newColor: *blue + modifyColor: *green + addColor: *grey + pendingColor: *orange + errorColor: *red + highlightColor: *yellow + killColor: *purple + completedColor: *grey + title: + fgColor: *blue + bgColor: *background + highlightColor: *purple + counterColor: *foreground + filterColor: *blue + views: + charts: + bgColor: *background + defaultDialColors: + - *green + - *red + defaultChartColors: + - *green + - *red + table: + fgColor: *yellow + bgColor: *background + cursorFgColor: *black + cursorBgColor: *blue + markColor: *yellow_bright + header: + fgColor: *grey + bgColor: *background + sorterColor: *orange + xray: + fgColor: *blue + bgColor: *background + cursorColor: *foreground + graphicColor: *yellow_bright + showIcons: false + yaml: + keyColor: *red + colonColor: *grey + valueColor: *grey + logs: + fgColor: *grey + bgColor: *background + indicator: + fgColor: *blue + bgColor: *background + toggleOnColor: *red + toggleOffColor: *grey + help: + fgColor: *grey + bgColor: *background + indicator: + fgColor: *blue diff --git a/skins/kiss.yml b/skins/kiss.yaml similarity index 100% rename from skins/kiss.yml rename to skins/kiss.yaml diff --git a/skins/monokai.yml b/skins/monokai.yaml similarity index 100% rename from skins/monokai.yml rename to skins/monokai.yaml diff --git a/skins/narsingh.yml b/skins/narsingh.yaml similarity index 100% rename from skins/narsingh.yml rename to skins/narsingh.yaml diff --git a/skins/nightfox.yml b/skins/nightfox.yaml similarity index 98% rename from skins/nightfox.yml rename to skins/nightfox.yaml index 6d144c69b6..61ec7dbd16 100644 --- a/skins/nightfox.yml +++ b/skins/nightfox.yaml @@ -99,5 +99,5 @@ k9s: indicator: fgColor: *foreground bgColor: *selection - toggleOnColor: *margenta + toggleOnColor: *magenta toggleOffColor: *blue diff --git a/skins/nord.yml b/skins/nord.yaml similarity index 100% rename from skins/nord.yml rename to skins/nord.yaml diff --git a/skins/one_dark.yml b/skins/one-dark.yaml similarity index 100% rename from skins/one_dark.yml rename to skins/one-dark.yaml diff --git a/skins/red.yml b/skins/red.yaml similarity index 100% rename from skins/red.yml rename to skins/red.yaml diff --git a/skins/rose-pine-dawn.yaml b/skins/rose-pine-dawn.yaml new file mode 100644 index 0000000000..da4edfd236 --- /dev/null +++ b/skins/rose-pine-dawn.yaml @@ -0,0 +1,110 @@ +# ----------------------------------------------------------------------------- +# Rose Pine Dawn +# https://rosepinetheme.com/palette/ingredients/ +# ----------------------------------------------------------------------------- +# +text: &text "#575279" +base: &base "#faf4ed" +overlay: &overlay "#f2e9e1" +muted: &muted "#9893a5" +rose: &rose "#d7827e" +pine: &pine "#286983" +gold: &gold "#ea9d34" +iris: &iris "#907aa9" +love: &love "#b4637a" + +# Skin... +k9s: + # General K9s styles + body: + fgColor: *text + bgColor: *base + logoColor: *iris + # Command prompt styles + prompt: + fgColor: *text + bgColor: *base + suggestColor: *iris + # ClusterInfoView styles. + info: + fgColor: *iris + sectionColor: *text + # Dialog styles. + dialog: + fgColor: *text + bgColor: *base + buttonFgColor: *text + buttonBgColor: *iris + buttonFocusFgColor: *gold + buttonFocusBgColor: *iris + labelFgColor: *gold + fieldFgColor: *text + frame: + # Borders styles. + border: + fgColor: *overlay + focusColor: *overlay + menu: + fgColor: *text + keyColor: *iris + # Used for favorite namespaces + numKeyColor: *iris + # CrumbView attributes for history navigation. + crumbs: + fgColor: *text + bgColor: *overlay + activeColor: *overlay + # Resource status and update styles + status: + newColor: *rose + modifyColor: *iris + addColor: *pine + errorColor: *love + highlightcolor: *gold + killColor: *muted + completedColor: *muted + # Border title styles. + title: + fgColor: *text + bgColor: *overlay + highlightColor: *gold + counterColor: *iris + filterColor: *iris + views: + # Charts skins... + charts: + bgColor: default + defaultDialColors: + - *iris + - *love + defaultChartColors: + - *iris + - *love + # TableView attributes. + table: + fgColor: *text + bgColor: *base + # Header row styles. + header: + fgColor: *text + bgColor: *base + sorterColor: *rose + # Xray view attributes. + xray: + fgColor: *text + bgColor: *base + cursorColor: *overlay + graphicColor: *iris + showIcons: false + # YAML info styles. + yaml: + keyColor: *iris + colonColor: *iris + valueColor: *text + # Logs styles. + logs: + fgColor: *text + bgColor: *base + indicator: + fgColor: *text + bgColor: *iris diff --git a/skins/rose-pine-moon.yaml b/skins/rose-pine-moon.yaml new file mode 100644 index 0000000000..df7e36317a --- /dev/null +++ b/skins/rose-pine-moon.yaml @@ -0,0 +1,110 @@ +# ----------------------------------------------------------------------------- +# Rose Pine Main +# https://rosepinetheme.com/palette/ingredients/ +# ----------------------------------------------------------------------------- +# +text: &text "#e0def4" +base: &base "#232136" +overlay: &overlay "#393552" +muted: &muted "#6e6a86" +rose: &rose "#ea9a97" +pine: &pine "#3e8fb0" +gold: &gold "#f6c177" +iris: &iris "#c4a7e7" +love: &love "#eb6f92" + +# Skin... +k9s: + # General K9s styles + body: + fgColor: *text + bgColor: *base + logoColor: *iris + # Command prompt styles + prompt: + fgColor: *text + bgColor: *base + suggestColor: *iris + # ClusterInfoView styles. + info: + fgColor: *iris + sectionColor: *text + # Dialog styles. + dialog: + fgColor: *text + bgColor: *base + buttonFgColor: *text + buttonBgColor: *iris + buttonFocusFgColor: *gold + buttonFocusBgColor: *iris + labelFgColor: *gold + fieldFgColor: *text + frame: + # Borders styles. + border: + fgColor: *overlay + focusColor: *overlay + menu: + fgColor: *text + keyColor: *iris + # Used for favorite namespaces + numKeyColor: *iris + # CrumbView attributes for history navigation. + crumbs: + fgColor: *text + bgColor: *overlay + activeColor: *overlay + # Resource status and update styles + status: + newColor: *rose + modifyColor: *iris + addColor: *pine + errorColor: *love + highlightcolor: *gold + killColor: *muted + completedColor: *muted + # Border title styles. + title: + fgColor: *text + bgColor: *overlay + highlightColor: *gold + counterColor: *iris + filterColor: *iris + views: + # Charts skins... + charts: + bgColor: default + defaultDialColors: + - *iris + - *love + defaultChartColors: + - *iris + - *love + # TableView attributes. + table: + fgColor: *text + bgColor: *base + # Header row styles. + header: + fgColor: *text + bgColor: *base + sorterColor: *rose + # Xray view attributes. + xray: + fgColor: *text + bgColor: *base + cursorColor: *overlay + graphicColor: *iris + showIcons: false + # YAML info styles. + yaml: + keyColor: *iris + colonColor: *iris + valueColor: *text + # Logs styles. + logs: + fgColor: *text + bgColor: *base + indicator: + fgColor: *text + bgColor: *iris diff --git a/skins/rose_pine.yml b/skins/rose-pine.yaml similarity index 100% rename from skins/rose_pine.yml rename to skins/rose-pine.yaml diff --git a/skins/snazzy.yml b/skins/snazzy.yaml similarity index 100% rename from skins/snazzy.yml rename to skins/snazzy.yaml diff --git a/skins/solarized-16.yml b/skins/solarized-16.yaml similarity index 100% rename from skins/solarized-16.yml rename to skins/solarized-16.yaml diff --git a/skins/solarized_dark.yml b/skins/solarized-dark.yaml similarity index 100% rename from skins/solarized_dark.yml rename to skins/solarized-dark.yaml diff --git a/skins/solarized_light.yml b/skins/solarized-light.yaml similarity index 100% rename from skins/solarized_light.yml rename to skins/solarized-light.yaml diff --git a/skins/stock.yml b/skins/stock.yaml similarity index 100% rename from skins/stock.yml rename to skins/stock.yaml diff --git a/skins/transparent.yml b/skins/transparent.yaml similarity index 98% rename from skins/transparent.yml rename to skins/transparent.yaml index be0a72e081..9a103a87fc 100644 --- a/skins/transparent.yml +++ b/skins/transparent.yaml @@ -7,7 +7,7 @@ k9s: body: bgColor: default - promt: + prompt: bgColor: default info: sectionColor: default diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index 682f054b40..d6ef0648a2 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -1,9 +1,9 @@ name: k9s -base: core20 -version: 'v0.27.4' +base: core22 +version: 'v0.32.5' summary: K9s is a CLI to view and manage your Kubernetes clusters. description: | - 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 you clusters in a single powerful session. + 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. grade: stable confinement: classic @@ -14,25 +14,15 @@ architectures: - armhf - i386 -plugs: - kube-config: - interface: personal-files - read: - - $HOME/.kube/config - apps: k9s: command: bin/k9s - plugs: - - network - - network-bind - - home - - kube-config parts: build: plugin: go - source: https://github.com/derailed/k9s.git + source: https://github.com/derailed/k9s + source-type: git source-tag: $SNAPCRAFT_PROJECT_VERSION override-build: | make test @@ -40,3 +30,5 @@ parts: install $SNAPCRAFT_PART_BUILD/execs/k9s -D $SNAPCRAFT_PART_INSTALL/bin/k9s build-packages: - build-essential + build-snaps: + - go